在上一篇工具调用中我们已经了解如何给 LLM 添加工具调用。为了加深理解工具调用如何扩展 LLM,现在来看更多工具调用的实践。这一节我们将赋予 LLM 搜索互联网的能力。
数据源
首先要考虑的是如何获得数据源。自建网络搜索引擎是个庞大的工程:爬虫、数据储存和海量信息检索。如果你就职于搜索引擎公司,那么可以使用内部的接口。而其他大部分开发者应该都需要使用第三方接口。
可惜的是,知名的搜索引擎公司,例如 Google、Bing 都不对外提供搜索 API,又或者跟自己的 AI 生态捆绑。所以我们可以把目光转向一些小型的以搜索 API 为产品的创业公司,例如:
- Tavily
- Exa
- Brave Search API
- 等等……
我不在这里做具体推荐,请自行判断这些服务的可靠性和价格。
Caution
不靠谱的数据源可能会带来提示词注入,或者返回结果里有非法内容,请谨慎选择。
为了讲解,下面内容会以 Tavily 为例子。
Tavily API
要使用 Tavily,先到管理面板获得 API Token,然后使用以下 API 进行搜索:
curl --request POST \
--url https://api.tavily.com/search \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"query": "who is Leo Messi?"
}
'
API 将会返回一个 JSON 数据:
{
"query": "Who is Leo Messi?",
"images": [],
"results": [
{
"title": "Lionel Messi Facts | Britannica",
"url": "https://www.britannica.com/facts/Lionel-Messi",
"content": "Lionel Messi, an Argentine footballer, is widely regarded as one of the greatest football players of his generation. Born in 1987, Messi spent the majority of his career playing for Barcelona, where he won numerous domestic league titles and UEFA Champions League titles. Messi is known for his exceptional dribbling skills, vision, and goal",
"score": 0.81025416,
"raw_content": null,
"favicon": "https://britannica.com/favicon.png",
"images": [
{
"url": "<string>",
"description": "<string>"
}
]
}
],
"response_time": "1.67",
"auto_parameters": {
"topic": "general",
"search_depth": "basic"
},
"usage": {
"credits": 1
},
"request_id": "123e4567-e89b-12d3-a456-426614174111"
}
其中 results 数组就是我们需要的搜索结果。
Tavily 官方没有提供 Ruby SDK,不过我们可以自己实现。
Ruby 实现
首先定义网络搜索工具:
TOOLS = [
{
name: "web_search",
schema: {
"type": "function",
"function": {
"name": "web_search",
"description": "Use this tool to search the web for the latest information.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query, e.g., 'who is Leo Messi?'"
}
},
"required": ["query"]
}
}
},
run: lambda do |args|
query = args["query"]
uri = URI("https://api.tavily.com/search")
# 通过环境变量 TAVILY_API_KEY 设置 API token
response = Net::HTTP.post(
uri,
{ query: query }.to_json,
{ "Content-Type" => "application/json", "Authorization" => "Bearer #{ENV["TAVILY_API_KEY"]}" }
)
if response.code.to_i == 200
data = JSON.parse(response.body)
# 只获取结果里的 title,url,content 内容
data["results"].map { |r| { title: r["title"], url: r["url"], content: r["content"] } }.to_json
else
"Tavily API error: #{response.code} - #{response.body}"
end
end
}
]
然后在调用 OpenAI 的时候,确保添加了 Tools 定义:
response = openai.chat.completions.create(
messages: messages,
model: "deepseek-v4-flash",
tools: TOOLS.map { |t| t[:schema] }
)
这样 LLM 在解答问题的时候就会根据需要使用网络搜索。
效果
用一些最近的新闻来提问,看 LLM 的回答。
没有网络搜索的情况:
Me> 2026世界杯第一场比赛结果
AI> 2026年世界杯(美加墨世界杯)将于2026年6月11日至7月19日举行。截至目前(2025年3月),比赛尚未开始,因此**第一场比赛的结果无法提供**。不过,根据国际足联已公布的赛程,首场比赛定于2026年6月11日在美国洛杉矶的SoFi体育场进行,具体对阵球队需待预选赛结束后(2025年底至2026年初)确定。建议你届时关注官方信息或媒体报道以获取最新结果。
LLM 只能回答到截止它的训练日期以前的数据。
有网络搜索的情况:
Me> 2026世界杯第一场比赛结果
[Tool call] web_search with arguments: {"query": "2026世界杯 第一场比赛 结果"}
AI> 根据最新信息,**2026年美加墨世界杯**已于当地时间2026年6月11日(北京时间6月12日)正式开幕。
🏆 **本届世界杯的揭幕战(第一场比赛)结果如下:**
| 对阵双方 | 比分 | 赛事类型 |
|---------|:----:|---------|
| 🇲🇽 **墨西哥** vs 🇿🇦 南非 | **2 : 0** | A组小组赛 |
这场揭幕战在墨西哥城体育场举行,东道主墨西哥以 **2-0** 击败南非,取得了开门红。值得一提的是,本场比赛场面相当激烈,共出现了 **3张红牌**,也是本届世界杯的首张红牌。
另外,同一天的另一场比赛中,东道主之一的 **加拿大** 在主场(多伦多)以 **1-1** 战平了波黑。
LLM 可以根据网络搜索获得的信息回答问题。
小结
这一节我们实现了网络搜索工具,给予了 LLM 获取最新资讯的能力。
完整代码
require "bundler/inline"
require "readline"
require "json"
require "net/http"
require "uri"
gemfile do
source "https://rubygems.org"
gem "openai"
end
openai = OpenAI::Client.new(
api_key: ENV["OPENAI_API_KEY"],
base_url: "https://api.deepseek.com"
)
TOOLS = [
{
name: "web_search",
schema: {
"type": "function",
"function": {
"name": "web_search",
"description": "Use this tool to search the web for the latest information.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query, e.g., 'who is Leo Messi?'"
}
},
"required": ["query"]
}
}
},
run: lambda do |args|
query = args["query"]
uri = URI("https://api.tavily.com/search")
# 通过环境变量 TAVILY_API_KEY 设置 API token
response = Net::HTTP.post(
uri,
{ query: query }.to_json,
{ "Content-Type" => "application/json", "Authorization" => "Bearer #{ENV["TAVILY_API_KEY"]}" }
)
if response.code.to_i == 200
data = JSON.parse(response.body)
# 只获取结果里的 title,url,content 内容
data["results"].map { |r| { title: r["title"], url: r["url"], content: r["content"] } }.to_json
else
"Tavily API error: #{response.code} - #{response.body}"
end
end
}
]
def execute_tool_calls(tool_calls)
tool_calls.map do |tool_call|
# 根据工具调用信息找到对应的工具
tool = TOOLS.find { |t| t[:name] == tool_call[:function][:name] }
next unless tool
# 输出工具调用信息
puts "[Tool call] #{tool[:name]} with arguments: #{tool_call[:function][:arguments]}"
# 执行工具并获取结果
args = JSON.parse(tool_call[:function][:arguments])
result = tool[:run].call(args)
# puts "[Tool result] #{result}"
# 将工具结果以特定格式追加到消息历史中,供模型后续使用
{
role: "tool",
tool_call_id: tool_call[:id],
content: result
}
end.compact
end
messages = []
loop do
input = Readline.readline("Me> ")
messages << { role: "user", content: input }
# 内层循环:持续将工具结果回传给模型,直到模型返回文本回复
loop do
response = openai.chat.completions.create(
messages: messages,
model: "deepseek-v4-flash",
# tools: TOOLS.map { |t| t[:schema] }
)
choice = response[:choices][0]
message = choice[:message]
if message[:tool_calls]
# 将助手的工具调用消息存入历史
messages << message
# 执行工具并将结果追加到历史
tool_results = execute_tool_calls(message[:tool_calls])
messages.concat(tool_results)
# 继续循环,让模型基于工具结果生成最终回复
else
content = message[:content] || ""
puts "AI> #{content}"
messages << message
break
end
end
end