快速回答
如何通过 function calling 给 OpenAI 模型加上网页搜索?
用 JSON schema 声明一个 search_web 函数,当模型调用它时,把查询 POST 到 LLMWebSearch 的 https://llmwebsearch.com/api/v1/search。把带编号的来源作为 function_call_output 返回,其中前 3 个页面为 Markdown;模型据此作答并引用。一次搜索花费 1 积分,截至 2026 年 10 月低至 US$1.20/千次搜索。无需 KYC;用加密货币支付。
你需要什么
| 软件包 | openai 和 requests(Agents SDK 版本则为 openai-agents) |
|---|---|
| 密钥 | OPENAI_API_KEY 用于模型,LLMWEBSEARCH_API_KEY 用于搜索 |
| 模型 | 任何支持 function calling 的模型,在 OPENAI_MODEL 中设置 |
| 使用的 API | Responses API:client.responses.create,配合 tools 和 input |
| 我方计费 | 1 积分支付一次查询:最多 20 条排名结果,其中前 3 条的页面会被读取并以 Markdown 返回。读取 4 到 10 个页面为 2 积分。以错误结束的调用不收费 |
工具 schema 长什么样?
一个 function tool 有 name、description 和 JSON Schema parameters。描述要具体:工具返回什么,以及来源应按编号引用。对大多数 agent 来说,两个参数就够了:query,以及完整返回多少个头部结果 read。
模型永远看不到你的 API 密钥。它只提出参数;由你的代码决定如何调用 API、返回什么。
调用循环如何工作?
把用户的消息连同工具一起发送。如果响应中包含类型为 function_call 的条目,解析每个条目的 JSON arguments,执行搜索,并追加一个具有相同 call_id 的 function_call_output。然后用更长的输入列表再次调用模型。当响应中不包含函数调用时,其文本就是答案。
下面的循环在五轮后停止,以防模型不停搜索。OpenAI 的 function-calling 指南描述了同样的 function_call 和 function_call_output 条目,于 2026 年 10 月 22 日读取。
import json
import os
import requests
from openai import OpenAI
client = OpenAI()
MODEL = os.environ["OPENAI_MODEL"] # any model that supports function calling
SEARCH = "https://llmwebsearch.com/api/v1/search"
tools = [
{
"type": "function",
"name": "search_web",
"description": "Search the live web. Returns numbered sources with URLs; the first `read` include page text. Cite sources as [1], [2].",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "What to search for, as a person would type it."},
"read": {"type": "integer", "minimum": 0, "maximum": 10, "description": "How many top results to read in full."},
},
"required": ["query"],
"additionalProperties": False,
},
}
]
def search_web(query, read=3):
resp = requests.post(
SEARCH,
headers={"Authorization": "Bearer " + os.environ["LLMWEBSEARCH_API_KEY"]},
json={"q": query, "max_results": 5, "read": read, "max_chars": 5000},
timeout=60,
)
if resp.status_code != 200:
return "Search failed: " + resp.json().get("error", {}).get("code", str(resp.status_code))
parts = []
for i, r in enumerate(resp.json()["results"], start=1):
text = r["content"] if r["content_status"] == "ok" else "(snippet only) " + r["snippet"]
parts.append(f"[{i}] {r['title']} - {r['url']}\n{text}")
return "\n\n".join(parts)
input_list = [{"role": "user", "content": "How should I size a Postgres connection pool?"}]
for _ in range(5):
response = client.responses.create(model=MODEL, tools=tools, input=input_list)
calls = [item for item in response.output if item.type == "function_call"]
if not calls:
print(response.output_text)
break
input_list += response.output
for item in calls:
args = json.loads(item.arguments)
output = search_web(args["query"], int(args.get("read", 3)))
input_list.append({"type": "function_call_output", "call_id": item.call_id, "output": output})
Agents SDK 有更短的版本吗?
有。OpenAI Agents SDK 通过一个装饰器把 Python 函数变成工具,并为你运行调用循环。其工具文档(于 2026 年 10 月 22 日读取)展示了来自 agents.decorators 的 @tool 装饰器,描述取自 docstring,每个参数的描述取自其 Args 部分。同一页面还提到了 @function_tool 形式。
Runner.run_sync 运行 agent 直到它给出最终答案,并按模型请求的次数调用 search_web。
import os
import requests
from agents import Agent, Runner
from agents.decorators import tool
@tool
def search_web(query: str, read: int = 3) -> str:
"""Search the live web and return numbered sources with their URLs.
Args:
query: What to search for, as a person would type it.
read: How many top results to read in full, from 0 to 10.
"""
resp = requests.post(
"https://llmwebsearch.com/api/v1/search",
headers={"Authorization": "Bearer " + os.environ["LLMWEBSEARCH_API_KEY"]},
json={"q": query, "max_results": 5, "read": max(0, min(read, 10)), "max_chars": 5000},
timeout=60,
)
resp.raise_for_status()
parts = []
for i, r in enumerate(resp.json()["results"], start=1):
text = r["content"] if r["content_status"] == "ok" else "(snippet only) " + r["snippet"]
parts.append(f"[{i}] {r['title']} - {r['url']}\n{text}")
return "\n\n".join(parts)
agent = Agent(
name="Researcher",
instructions="Answer from search_web results only and cite them as [1], [2].",
tools=[search_web],
)
result = Runner.run_sync(agent, "How should I size a Postgres connection pool?")
print(result.final_output)
为什么用 API 工具而不用 OpenAI 内置的网页搜索?
主要是为了控制模型读取的内容以及花费。我们的工具把原始来源直接交给你的代码,带 URL 和网页正文,因此由你决定哪些内容进入上下文,也能记录和存储。我们不会用你的查询或 API 返回的内容训练任何模型,也不会自行添加任何禁止存储结果的规则;你读取的页面所属网站的权利仍然适用。
价格也是按每次查询固定收费,而不取决于模型读取了多少检索内容。内置工具对比把 OpenAI、Anthropic、Google 等的每次调用价格并排列出,并标注了日期。
如何避免模型搜索过多?
在指令里说清楚:只搜索一次,阅读来源,只有在来源互相矛盾或没有切中要点时才再搜索。在自己的循环里限制轮数,如上面的脚本限制为五轮。
让 max_chars 保持适中。三页、每页 5,000 字符,约合每次查询 4,000 tokens,而每一轮都会把它们加到下一次模型调用的输入里。
OpenAI function calling:常见问题
这在 Chat Completions API 上能用吗?
可以,用的是 Chat Completions 较旧的 tools 格式,函数放在 function 对象里,结果以 tool 消息返回。搜索函数本身不变。
模型能读取在来源中发现的页面吗?
再加一个函数 read_urls,把 URL 提交给 https://llmwebsearch.com/api/v1/extract。读取你已持有 URL 的页面:每开始一组 3 个返回内容的页面计 1 积分,每次调用最多 20 个 URL;被拒绝或失败的 URL 不计入。
该用哪个模型?
任何支持 function calling 的模型都可以。拿你自己的问题试两三个:答案既取决于来源,也取决于模型,而更小的模型每轮成本更低。
来源 (2)
- Function calling — OpenAI API docs (read 2026-10-22)
- 工具(OpenAI Agents SDK) — OpenAI Agents SDK docs (read 2026-10-22)