【从0搭建AI智能体·7】给 Agent 接搜索:实时联网问答,突破知识截止日期
【从0搭建AI智能体·7】给 Agent 接搜索:实时联网问答,突破知识截止日期
📚 本文是《从 0 搭建你的 AI 智能体》专栏第 7 篇。
上一篇:《多轮对话状态管理与并发安全》标签:
联网搜索AI AgentFunction Calling网页抓取大模型RAG
📌 前言:模型的「记忆」停在了过去
你问大模型「今天有什么新闻」「最新版的 Python 是多少」「某公司昨天的股价」,它要么回答一个过时的答案,要么老实说「我的知识截止到某年某月」。
这是所有大模型的天生局限:它的知识冻结在训练数据的截止日期,之后发生的一切它一无所知。对于新闻、股价、天气、最新文档、实时数据这类问题,光靠模型自己,注定答不好。
解法就是给它接上「眼睛」——实时联网搜索。当模型判断需要最新信息时,让它调用搜索引擎,把搜到的内容读进来,再基于真实的实时资料作答。这其实是第 3 篇 Function Calling + 第 2 篇 RAG 思路的一次实战组合:用工具调用触发搜索,用检索到的内容增强生成。
这一篇,我们从零搭一个能联网的问答 Agent,并处理好「搜索→抓取→喂给模型→标注来源」这条完整链路里的每个坑。
💡 本文适合谁:已掌握 Function Calling 基础、想让 Agent 回答实时问题的开发者。阅读约 13 分钟。
目录
- 联网问答的完整链路
- 第一步:选一个搜索 API
- 第二步:封装搜索工具
- 第三步:抓取网页正文(可选但推荐)
- 第四步:用 Function Calling 让模型自己决定搜不搜
- 第五步:把结果喂回模型 + 标注来源
- 完整可跑 Demo
- 进阶:搜索质量优化
- 常见坑与排查
- FAQ
- 总结
① 联网问答的完整链路
联网问答不是「模型直接上网」,而是一条你搭的流水线:
用户提问
│
▼
模型判断:需要实时信息吗?
├─ 否 → 直接用自身知识回答
└─ 是 → 调用 search 工具
│
▼
搜索 API 返回 Top-N 结果(标题+摘要+链接)
│
▼
(可选)抓取网页正文,拿到更完整内容
│
▼
把搜索结果作为「参考资料」回传给模型
│
▼
模型基于实时资料组织答案 + 标注来源链接
│
▼
有据可查的实时回答
看出来了吗——这就是第 3 篇的工具调用(模型决定搜不搜)+ 第 2 篇的 RAG(把资料喂回去增强生成)的合体。学过前面,这篇就是水到渠成。
② 第一步:选一个搜索 API
模型不能直接爬 Google,你得给它一个能用 API 调的搜索服务。常见选择:
| 搜索 API | 特点 | 适用 |
|---|---|---|
| Tavily | 专为 LLM 设计,直接返回干净摘要,免爬网页 | AI 应用首选,最省事 |
| SerpAPI | 封装 Google 结果,功能全 | 要 Google 原始结果 |
| Bing Web Search | 微软官方,稳定 | 企业级 |
| DuckDuckGo(免费库) | duckduckgo_search 库,免 Key | 快速原型、练手 |
本文用 Tavily 演示(专为 LLM 优化、返回结果干净),思路对其他 API 通用。
pip install tavily-python openai python-dotenv
.env 加上搜索 Key:
AGENT_KEY=sk-xxx
API_BASE_URL=https://api.example.com/v1
TAVILY_API_KEY=tvly-xxx
💡 为什么推荐 Tavily 这类「AI 原生」搜索:传统搜索 API 返回一堆链接,你还得自己爬网页、清洗正文。Tavily 直接返回提炼过的摘要,省掉第 ④ 步的大量麻烦。原型阶段能省一半功夫。
③ 第二步:封装搜索工具
先把搜索封装成一个普通函数,返回「标题 + 摘要 + 链接」的结构化结果:
import os
from tavily import TavilyClient
from dotenv import load_dotenv
load_dotenv()
tavily = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
def web_search(query, max_results=5):
"""联网搜索,返回结构化结果列表"""
resp = tavily.search(query=query, max_results=max_results)
results = []
for item in resp.get("results", []):
results.append({
"title": item.get("title", ""),
"content": item.get("content", ""), # Tavily 已提炼的摘要
"url": item.get("url", ""),
})
return results
if __name__ == "__main__":
for r in web_search("2026 年 Python 最新稳定版本"):
print(f"- {r['title']}\n {r['content'][:80]}...\n {r['url']}\n")
⚠️ 避坑:搜索结果可能很长,全部塞给模型会爆 Token。上面只取摘要(
content),并可在下一步限制总长度。别把几万字原文整坨喂给模型。
④ 第三步:抓取网页正文(可选但推荐)
如果搜索 API 只给了摘要、而你需要更完整的内容(比如读一篇长文),就得抓网页正文。用 requests + readability 提取核心内容,去掉导航、广告等噪声:
pip install requests readability-lxml lxml_html_clean
import requests
from readability import Document
def fetch_page_text(url, max_chars=3000):
"""抓取网页并提取正文,去除导航/广告等噪声"""
try:
html = requests.get(url, timeout=10, headers={
"User-Agent": "Mozilla/5.0" # 伪装浏览器,避免被拒
}).text
doc = Document(html)
# readability 提取主体内容
from lxml import html as lxml_html
content = lxml_html.fromstring(doc.summary()).text_content()
return content.strip()[:max_chars] # 截断,控制长度
except Exception as e:
return f"[抓取失败: {e}]"
🎯 要不要抓正文,看场景:
- 问「最新版本号」这种事实 → 摘要就够,不用抓正文;
- 问「详细解读某篇长文」→ 需要抓正文拿完整内容。
抓正文慢(每个 URL 一次 HTTP + 解析),能不抓就不抓。Tavily 这类已经给了不错的摘要,多数场景直接用摘要即可。
⑤ 第四步:用 Function Calling 让模型自己决定搜不搜
关键一步:不是每个问题都要联网。「1+1 等于几」「帮我写首诗」根本不需要搜。让模型用 Function Calling 自己判断该不该搜(复习第 3 篇):
import json
from openai import OpenAI
client = OpenAI(api_key=os.getenv("AGENT_KEY"), base_url=os.getenv("API_BASE_URL"))
MODEL = "standard-agent-v1"
SEARCH_TOOL = [{
"type": "function",
"function": {
"name": "web_search",
"description": "联网搜索实时信息。当问题涉及最新新闻、实时数据、"
"近期事件,或超出你知识截止日期的内容时使用。",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索关键词"}
},
"required": ["query"],
},
},
}]
🎯
description决定成败:这里明确写「当涉及最新信息、超出知识截止日期时使用」,模型才能准确判断。写得含糊,它要么该搜不搜、要么啥都搜(浪费钱和时间)。
⑥ 第五步:把结果喂回模型 + 标注来源
搜到结果后,作为「参考资料」回传给模型,并要求它标注来源链接(这是联网问答区别于瞎编的关键——答案可溯源):
def answer_with_search(question):
messages = [
{"role": "system", "content":
"你是能联网的助手。当使用搜索结果回答时,必须在末尾以「参考来源:」"
"列出用到的链接,确保答案可追溯。若搜索结果不足以回答,如实说明。"},
{"role": "user", "content": question},
]
# 第 1 轮:模型判断是否要搜
resp = client.chat.completions.create(
model=MODEL, messages=messages, tools=SEARCH_TOOL, tool_choice="auto")
msg = resp.choices[0].message
if not msg.tool_calls:
return msg.content # 不需要搜,直接回答
# 执行搜索
messages.append(msg)
for tc in msg.tool_calls:
query = json.loads(tc.function.arguments)["query"]
results = web_search(query)
# 把结果整理成带链接的文本
context = "\n".join(
f"[{i+1}] {r['title']}\n{r['content']}\n来源: {r['url']}"
for i, r in enumerate(results))
messages.append({
"role": "tool", "tool_call_id": tc.id, "content": context})
# 第 2 轮:模型基于搜索结果作答
final = client.chat.completions.create(model=MODEL, messages=messages)
return final.choices[0].message.content
🎯 强制标注来源的意义:联网问答最大的价值是「可信」。要求模型列出参考链接,用户能自己点进去核实,这比一个无从考证的答案可信得多——也是和「模型瞎编」的本质区别。
⑦ 完整可跑 Demo
整合成一个能联网的问答脚本:
import os, json
from openai import OpenAI
from tavily import TavilyClient
from dotenv import load_dotenv
load_dotenv()
client = OpenAI(api_key=os.getenv("AGENT_KEY"), base_url=os.getenv("API_BASE_URL"))
tavily = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
MODEL = "standard-agent-v1"
TOOLS = [{"type": "function", "function": {
"name": "web_search",
"description": "联网搜索实时信息。涉及最新新闻、实时数据、超出知识截止日期时使用。",
"parameters": {"type": "object",
"properties": {"query": {"type": "string"}}, "required": ["query"]}}}]
def web_search(query):
resp = tavily.search(query=query, max_results=5)
return "\n".join(
f"[{i+1}] {r['title']}\n{r['content']}\n来源: {r['url']}"
for i, r in enumerate(resp.get("results", [])))
def ask(question):
messages = [
{"role": "system", "content":
"你是能联网的助手。用搜索结果回答时,末尾以「参考来源:」列出链接。"},
{"role": "user", "content": question},
]
msg = client.chat.completions.create(
model=MODEL, messages=messages, tools=TOOLS, tool_choice="auto"
).choices[0].message
if not msg.tool_calls:
return msg.content
messages.append(msg)
for tc in msg.tool_calls:
q = json.loads(tc.function.arguments)["query"]
print(f"[联网搜索] {q}")
messages.append({"role": "tool", "tool_call_id": tc.id, "content": web_search(q)})
return client.chat.completions.create(model=MODEL, messages=messages).choices[0].message.content
if __name__ == "__main__":
print(ask("2026 年有什么值得关注的 AI 大事件?")) # 会触发搜索
print("---")
print(ask("帮我写一句关于春天的诗")) # 不会搜,直接答
⑧ 进阶:搜索质量优化
搜出来的东西质量参差,几个提升点:
| 优化项 | 说明 |
|---|---|
| 查询改写 | 用户口语化提问(「那个新出的模型咋样」)先让模型改写成精准搜索词 |
| 结果去重/筛选 | 多个来源内容重复时去重;明显低质的站点过滤掉 |
| 时效性优先 | 新闻类问题,优先取近期结果(部分 API 支持按时间排序) |
| 多轮搜索 | 一次搜不够,允许模型基于首轮结果再搜(配合第 3 篇的循环) |
| 控制总长度 | 所有结果拼接后做长度上限,避免爆 Token |
💡 查询改写是性价比最高的一招:用户的话往往不适合直接当搜索词。让模型先「翻译」成一个好的搜索 query,召回质量立竿见影。
⑨ 常见坑与排查
| 现象 | 原因 | 解决 |
|---|---|---|
| 该搜的时候不搜 | 工具 description 没写清时机 | 明确「涉及最新信息时使用」 |
| 啥都搜(浪费钱) | 同上,或 tool_choice 设成了 required | 用 auto,让模型自己判断 |
| Token 超限报错 | 搜索结果太长整坨塞入 | 取摘要 + 截断 + 限制条数 |
| 网页抓取失败/超时 | 反爬、超时 | 加 UA、设超时、失败兜底不影响主流程 |
| 答案不标来源 | System Prompt 没要求 | 明确要求「列出参考链接」 |
| 搜索结果过时 | 未按时效排序 | 用支持时间过滤的 API 参数 |
🔍 调试技巧:把模型每次生成的
query打印出来(Demo 里已加)。如果发现它搜的关键词很差,问题多半在「查询改写」或description上。
⑩ FAQ
Q1:一定要用付费搜索 API 吗?
原型练手可用免费的 duckduckgo_search 库(免 Key)。但免费方案稳定性、结果质量一般,生产建议用 Tavily/Bing 等。
Q2:联网了模型还会幻觉吗?
会减少但不能根除。关键是 System Prompt 约束「只基于搜索结果、标注来源」,并在结果不足时让它说「找不到」,而非硬编。
Q3:搜索 + RAG 知识库能一起用吗?
能,而且常一起用:私有知识走 RAG(第2篇),公开实时信息走联网搜索,模型按需选择工具。
Q4:怎么控制搜索成本?
① 让模型判断该不该搜(别每次都搜);② 限制 max_results;③ 对相同查询做缓存。
Q5:抓网页会有法律/合规风险吗?
遵守目标站点的 robots.txt 和使用条款,控制频率,不抓取受限内容。商用前建议用正规搜索 API 而非自行大规模爬取。
⑪ 总结
这一篇给 Agent 装上了「实时的眼睛」:
| 环节 | 关键点 |
|---|---|
| 搜索源 | 选 AI 原生搜索(Tavily)最省事 |
| 触发 | 用 Function Calling 让模型自己判断该不该搜 |
| 内容 | 摘要够用就别抓正文;结果要截断控长 |
| 生成 | System Prompt 强制标注来源,可追溯 |
| 优化 | 查询改写性价比最高 |
本质上,联网问答 = 工具调用(第3篇)+ RAG 增强(第2篇) 的组合应用。你会发现,越往后走,越是在把前面的积木拼成更强大的东西——这正是「搭建智能体」的乐趣所在。
下一篇我们进入 Agent 的「大脑」层面:ReAct vs Plan-Execute,两种主流的智能体编排模式对比,让 Agent 学会规划多步骤复杂任务。
🔜 下一篇预告:《ReAct vs Plan-Execute:Agent 编排模式对比》——让智能体学会拆解和规划复杂任务。
👍 如果本文帮到你,点赞 / 收藏 / 关注,追更不迷路。
更多推荐
所有评论(0)