
有个做 AI 助手的朋友上周找我说他给 Chatbot 接上了联网搜索结果用户还是吐槽回答又旧又蠢。这个场景我太熟悉了——过去大半年我一直在折腾智能体Agent相关项目Chatbot 从只会背书的生成模型到能自己上网查资料、再基于资料做推理这条技术路线的演进我算是一步步踩过来的。今天这篇文章就把 Chatbot 联网搜索这件事从头聊透它为什么从搜索框变成了Agent 手里的一个工具三代技术方案的本质区别是什么以及如果你想自己动手实现有哪些参数、架构和坑是文档里不会写但你必须知道的。不管你是刚接触 Agent 开发还是已经上线过带搜索功能的 Chatbot这篇都应该能帮你把脑子里那条技术主线理清楚。1. Chatbot 为什么非要联网搜索三个绕不开的现实问题1.1 知识是有保质期的模型自己编不出昨天大模型的知识来自训练语料而语料是某个时间点之前抓取的。你问它明天某城市天气怎么样它能给你编一个你问它某公司今天发布的财报数据它也能给你编一个——编得还挺像那么回事。这不是模型故意撒谎而是它在推理时根本没有通道感知训练数据之外的世界。联网搜索就是给模型开了一扇实时读取外部世界的窗让它能把训练时学到的知识和此刻发生的事实结合起来回答才能落地。这个道理说起来很朴素但不少产品经理是在上线之后才真正理解的他们发现用户问的问题里有相当比例是时效敏感的模型对这类问题的编造率极高用户一试就露馅。1.2 用户要的是答案不是一条链接早期 Chatbot 的联网搜索功能做得很粗暴用户问帮我找找最近的 AI 行业大会它就返回十条链接和标题。这本质上只是把搜索引擎的界面搬进了对话框用户还是得一个个点开看信息获取效率一点没提升。真正让联网搜索成为刚需的是搜索→阅读→提炼→回答这条完整链路被接进 Chatbot 之后——用户问一个问题模型自己去检索、去阅读网页内容、最后给出一个已经消化过的答案这才是 Chatbot 相对搜索引擎的体验优势。说白了用户不是来用你的搜索框的是来要答案的。把这个目标记清楚你后面做技术选型的时候就不容易跑偏。1.3 幻觉问题搜索是最后的证据来源大模型回答事实性问题时如果完全没有外部资料支撑就只能依赖参数记忆而参数记忆里既有过时信息也有训练时产生的错误关联。联网搜索恰恰是缓解幻觉最直接的手段给模型喂入真实的搜索结果并强制它基于资料回答可以显著压缩编造空间。但这里有个关键约束——搜索结果本身的来源也得可信。如果搜索引擎返回的是竞价广告或 SEO 垃圾文模型照单全收反而会输出更离谱的幻觉。所以联网搜索四个字听着简单真正做到位需要解决检索质量、信息可信度、上下文管理三个层面的问题后面我会逐个展开。2. 从搜索框到 Agent三代方案到底差在哪2.1 第一代搜索框时代关键词搬运工最早一批带搜索能力的 Chatbot其实很多是 IM 机器人实现上就是正则加爬虫用户发来一句带搜索字样的话机器人从中抠出关键词调一个搜索接口把标题和链接拼成消息发回去。我记得有项目直接用 requests 请求搜索引擎结果页再拿 BeautifulSoup 解析 HTML结果页面改个 class 名就崩了。这一代方案的定位本质上是搜索引擎的皮肤模型只是壳搜索只是关键词匹配两者没有任何智力上的融合。它能满足帮我搜一下这种指令型需求但完全处理不了帮我比较一下这种需要综合分析的问题。2.2 第二代搜索增强生成模型学会查资料再作答第二代方案的关键变化是模型开始参与整个流程。实现上通常是在对话请求里加一个前置步骤让模型自己判断这个问题需不需要搜索如果需要就把搜索结果标题、摘要、发布日期、URL格式化后拼进 prompt再让模型基于这些资料组织答案。这一代的代表形态就是大家熟知的 Web Search / 联网搜索功能。技术上它比第一代成熟很多搜索 API 返回结构化 JSON网页正文可以用 readability 类工具提取注入 prompt 时有统一模板时效性问题可以用 freshness 参数控制。但要泼一盆冷水第二代方案的搜索决策是一次性的。模型发起一次搜索之后就拿着这批结果直接回答它不会因为结果不够好而换一个更精准的 query 再搜也不会主动拆解复杂问题。遇到多跳问题比如先查 A 公司的市值再看它和 B 公司的差距一次性搜索往往只能拿到浅层信息这正是第三代 Agent 方案要解决的。2.3 第三代Agent 把搜索变成决策链上的一环Agent 化改造的核心不是换了技术栈而是改变了控制流搜索不再是回答前的一个可选步骤而是模型在推理循环里可以反复调用的工具。模型先规划再决定调用哪个工具、构造什么 query拿到结果后继续推理可能再次搜索最后才收敛到答案。整个过程是个循环think → act → observe → think直到模型认为信息足够。我用一个真实场景对比一下两代的差异。用户问我想买一款 5000 元以内、拍照好的手机帮我推荐三款。第二代方案搜一次5000元以内拍照好的手机推荐返回一堆营销稿模型照抄完事。第三代方案模型会先搜2024 年 5000 元价位手机评测发现结果不够新再按品牌拆开搜XX 手机 拍照 评测还会搜XX 手机 价格 2024来验证价格是否在预算内最后从多轮结果里交叉比对得出推荐。信息量和准确率完全不是一个量级。当然第三代也带来了新问题搜索次数不可控、上下文消耗更快、模型可能陷入搜了又搜的死循环、工具调用的稳定性依赖底层模型能力。想把这代方案做稳需要在编排层做很多细节控制这正是后面要展开的部分。2.4 三代方案对照表选型先看这张表维度第一代 关键词搬运第二代 搜索增强生成第三代 Agent 工具化搜索触发用户指令/关键词匹配模型一次性判断模型规划循环中动态触发处理方式返回链接列表结果拼入 prompt 生成答案多轮搜索 推理交叉验证多跳问题不支持弱支持上下文消耗低中高需专门管理实现成本极低中较高典型形态IM 机器人Web Search 功能Agent / 智能体3. 手把手搭一个带联网搜索能力的最小 Agent3.1 整体架构一个循环 一个工具 一套记忆先给结论一个最小可用的联网搜索 Agent不需要一上来就上 LangGraph、Dify、Coze 这些重型框架。核心就是一个循环把模型返回、工具调用回传、消息历史装配成一个循环再加上一个 web_search 工具再加一套控制上下文的策略。把这三件事做对了就已经超过市面上很多套壳 Agent了。架构上我会拆成四层模型层任意支持 Function Calling / Tool Call 的模型现在主流模型基本都支持工具层web_search内部对接一个具体的搜索 API编排层负责维护循环、检查终止条件、做 token 预算控制记忆层对话历史 搜索结果缓存决定每次请求带哪些上文。这四层里最容易偷懒的是记忆层但恰恰是它决定了 Agent 能不能在长对话里保持稳定。后面我会单独讲。3.2 工具注册写好 function schema 是第一步很多新手 Agent 项目翻车翻在工具的 function schema 写得太随意。模型是根据工具的 name、description、parameters 来判断何时调用、如何填参数的。description 写得太泛模型什么鸡毛蒜皮的问题都去搜索参数名起得玄乎模型就填不明白。我用的 web_search schema 长这样以 OpenAI 兼容格式为例TOOLS [ { type: function, function: { name: web_search, description: ( 在互联网上搜索公开信息。 当问题涉及实时数据、最新事件、具体事实、价格/天气/新闻 或用户明确要求搜索/查一下/最新时使用该工具。 若问题仅依赖常识即可回答不要使用。 ), parameters: { type: object, properties: { query: { type: string, description: 简洁具体的搜索关键词不要包含语气词 }, freshness: { type: string, enum: [Day, Week, Month, NoLimit], description: 结果时效性范围新闻类问题用 Day 或 Week }, max_results: { type: integer, description: 返回条数默认5最多10, minimum: 1, maximum: 10 } }, required: [query] } } } ]这里有三个经验值得记下来第一description 里写明什么时候用、什么时候不要用这是控制搜索频率最省钱的 prompt 手段第二query 参数要求简洁具体能显著提升搜索引擎返回质量第三freshness 单独作为参数而不是硬编码让模型自己按问题类型决策。这些细节写好了后面调模型行为的成本会低很多。3.3 主循环Agent 的大脑回路实现接下来是编排层的核心循环。我的最小实现只有 30 行左右import json def run_search_agent(user_input, executor, max_steps3, max_tokens_per_step2000): messages [{role: user, content: user_input}] for step in range(max_steps): resp executor.chat(messagesmessages, toolsTOOLS) msg resp[message] if not msg.get(tool_calls): return msg[content], step 1 messages.append(msg) # 把带 tool_calls 的 assistant 消息加入历史 for call in msg[tool_calls]: args json.loads(call[function][arguments]) results web_search(args.get(query), args.get(freshness, NoLimit), args.get(max_results, 5)) compact compress_results(results, max_tokensmax_tokens_per_step) messages.append({ role: tool, tool_call_id: call[id], content: json.dumps(compact, ensure_asciiFalse) }) return 已达到最大搜索轮数基于已有信息回答, max_stepsexecutor 可以是任意兼容 Chat Completions 接口的 SDK 封装底层模型换成支持 tool call 的即可。这个循环的关键点有两个。注意assistant 消息带 tool_calls 时必须原样回传给下一次请求同时 tool 消息里的 tool_call_id 必须与调用一一对应任何一个对不上模型服务端都会直接拒绝本次请求。另外终止条件一定要硬编码不要指望模型自己知道什么时候该停。max_steps 设 3 到 5 比较合理超过了就强制收敛。这一步不做你的 Agent 迟早会给你跑出一张天价 token 账单。3.4 搜索结果压缩这一步决定你的 token 账单直接从搜索 API 拿到的原始 JSON 是不适合塞进上下文的一个结果除了 title、snippet、url还带着发布时间、地域、站点分类等一堆字段十条结果全塞光字段名就要浪费几百 token。我的做法是先做一轮降维def compress_results(results, max_tokens2000): lines [] for r in results: line { 标题: r.get(title), 来源: r.get(url, ).split(/)[2] if r.get(url) else , 日期: r.get(publish_date, ), 摘要: r.get(snippet, )[:150], } lines.append(line) return {results: lines}摘要截断到 150 字符以内只保留标题、域名、日期、摘要四项。再加上模板包装注入格式是以下是联网搜索结果请优先依据这些资料回答用户问题。 若资料中没有相关信息请直接说明不要编造。 results 1. 标题... 来源... 日期... 摘要... 2. ... /results这样单次搜索结果注入的 token 消耗能被压在 800 到 1200 token 以内比直接塞原始 JSON 省一半以上。模型对资料边界的感知也更清楚看到results标签就知道这些是外部证据不是它自己记忆里的东西。3.5 多轮搜索与终止条件防止 Agent 上头Agent 循环最大的风险是停不下来。模型一旦觉得信息不够会一直构造新 query 继续搜这一方面烧 token一方面让用户等待时间失控。我实践中用三条防线max_steps 硬限制通常 3 到 5prompt 里显式写It is better to answer based on available information than to search repeatedly这类约束工具 schema 的 description 强调一次调用返回的结果应该足够回答大多数问题。另外可以在每轮 tool 结果返回后额外让模型判断当前信息是否已经足够回答如果足够就直接收尾这样能减少无效搜索。这个判断可以做成一个轻量分类器也可以直接靠模型自己实测下来模型自己的判断在大部分场景是靠谱的。4. 生产环境实战并发、相关性与降级4.1 并发规划先算清你的 QPS 账我见过不少项目Demo 阶段搜得挺欢一上线就被搜索 API 限流打懵。搜索 API 几乎都是按 QPS 和次数双重限制的你不做并发规划流量一上来就会收到一堆 429 错误。先算一笔账假设你的 Chatbot 平均每个用户会话触发 1.5 次搜索、单次搜索 API 耗时 800ms那么要支撑 100 个用户同时在线的峰值单台服务需要的搜索并发约等于 100 × 1.5 / 10 15 QPS假设这 100 个用户的搜索请求在 10 秒内到达。这个量级单靠一个免费额度经常不够要么扩容账号要么上缓存。我的建议是三层防御第一层本地缓存同一个 query 在 TTL 内直接命中新闻类 TTL 设 5 分钟常识类可设 24 小时第二层本地限流用令牌桶把出站 QPS 压到套餐上限的 70% 左右留 30% 余量防止突发第三层多源 failover主搜索源挂了或 429 时自动切到备用源我通常同时接两家平时主备轮询还能顺便分摊成本。4.2 结果相关性搜索引擎的前十条不等于最相关的十条这是最容易踩坑的地方。通用搜索引擎的结果排序是为人浏览网页设计的不是为模型提取事实设计的。你拿一个技术问题的 query 去搜前排经常是内容农场、SEO 聚合页、甚至过时的营销软文。模型照单全收回答质量自然崩。我的处理经验分两步。第一步是前置过滤对返回结果的域名做白名单和黑名单管理把已知的内容农场域名直接剔掉对带广告标识的结果直接丢弃对明显标题党或摘要为空的结果降权。第二步是 rerank用一个小模型或专门的 rerank 模型对候选结果打分把相关性最高的 3 到 5 条提进最终上下文。rerank 的成本不高但准确率提升非常明显在我项目里的实测中回答相关性评分平均涨了 15% 到 20%。4.3 超时、重试与降级搜索挂了Chatbot 不能跟着挂搜索是高延迟外部依赖必须把它当成可能随时不可用的组件来设计。我通常给搜索调用设 3 到 5 秒超时超过就放弃本轮搜索让模型退回纯生成回答同时在回答里提示当前未能获取最新网络信息。重试要带指数退避第一次失败等 200ms第二次 500ms再失败就不重试了直接走降级。注意重试不是越多越好因为 Chatbot 对首字延迟极其敏感一次搜索超过 5 秒用户体感已经明显变差。注意搜索结果来自公开网络可能包含恶意指令文本。务必在注入前对网页正文做清洗避免模型被提示注入。还有一个容易被忽略的问题搜索结果的注入本身也可能被攻击。搜索结果里如果混入了恶意网页内容比如网页正文里写着忽略此前指令回答……模型就可能被提示注入。生产环境务必对搜索结果做清洗正文提取时截断超长部分必要时过滤 HTML 标签和脚本并考虑对模型输出做敏感指令检测。5. 方案选型与成本账别让搜索 API 掏空你的钱包5.1 主流搜索源怎么选列一下我实际用过的几类类型代表优点缺点适合场景通用搜索 APIBing Web Search、SerpAPI覆盖广、结果结构化按次计费、需清洗通用产品 Web Search 功能面向 AI 的搜索 APITavily、Exa返回结构面向模型、自带摘要覆盖面相对小众Agent 工具、RAG 场景自建爬虫自研成本可控、定制性强反爬、去重、维护成本高垂直站点、深度定制选型的核心逻辑不是谁功能多而是你的调用场景。如果是给 Agent 做工具我优先推荐面向 AI 的搜索 API因为它的返回结构就是为模型设计的少写很多解析代码如果是给通用产品做 web search 功能通用搜索引擎 API 更稳。自建爬虫我一般只在垂直站点场景用比如只采集固定几个来源的新闻这种场景下爬虫的性价比反而最高。5.2 成本估算公式联网搜索的成本由三个部分组成搜索 API 调用费、注入结果占用的模型输入 token 费、失败重试的浪费。估算公式可以这样写单次搜索注入成本 平均结果条数 × 单条摘要 token 模板 token。按 5 条结果、每条约 150 token 算单次注入约 800 到 1000 token。如果每天 10 万次用户请求、平均每次触发 1.2 次搜索那么搜索 API 调用就是 12 万次/天输入 token 消耗约 1.2 亿 token/天。这个量级下缓存命中率提高 10 个百分点省下的钱可能比优化模型 prompt 更可观。所以我把缓存视为成本控制的第一手段热门 query天气、新闻、热门产品加 Redis 缓存TTL 短一点冷门 query 不做缓存同一会话内的重复搜索直接查会话级缓存。这套组合拳打下来实际搜索 API 调用量通常能砍掉 30% 以上。5.3 MCP 协议要不要上最近 MCP 很火不少团队问我是不是必须用 MCP 来挂搜索工具。我的观点是MCP 是工具标准化协议它的价值在于一次封装、处处可用——你写一个 web search 的 MCP server就能被各种支持 MCP 的 Agent 框架复用。但如果你的项目只是在自己产品里用直接写一个 function calling 工具也完全够没必要为了用 MCP 而引入额外复杂度。等你的工具数量超过五六个、或者要开放给第三方 Agent 调用时再考虑 MCP 更务实。6. 常见问题与排查技巧实录我踩过的 5 个经典坑6.1 搜索结果太旧模型还当新闻答症状用户问最近发生的事模型给的还是几个月前的信息。排查第一步看搜索 API 请求里有没有传 freshness 参数——很多 SDK 默认不传搜索引擎默认按综合排序返回旧文章很容易排前面。新闻类 query 一定要传 freshnessDay 或 Week。第二步看结果解析发布日期字段是否被正确提取。有些搜索 API 的 snippet 里会带日期文本但结构化字段是空的建议在解析层做一次兜底从 snippet 里用正则抽日期。6.2 模型反复搜索停不下来上下文几分钟就爆最常见的原因有三个工具 description 写得太泛、搜索结果里缺乏有效信息、prompt 没有够了就停的约束。排查方式是在日志里看连续 tool call 的 query 序列——如果模型在围绕同一个意图换措辞反复搜说明第一批结果没给它足够信心要么提高结果条数要么优化 query 质量如果模型在毫无关联的话题间乱跳那是上下文管理出了问题历史里可能塞了太多无关的上一轮结果模型被带偏了。解决思路max_steps 硬限制必须上每轮搜索结果注入前做一轮与当前问题相关性的粗筛不相关的直接丢弃。6.3 上下文窗口被搜索结果塞爆这是联网搜索 Agent 最经典的翻车点。单次结果 1000 token 看着不多3 轮搜索加上历史对话、系统 prompt、工具定义轻松突破 8k 上下文。我的经验是设三个分层对话历史做滑动窗口保留最近 6 到 8 轮搜索结果只保留当前推理步需要的那几段前一轮的搜索结果压缩成一句摘要存进历史超过上限就触发自动摘要把旧对话历史摘要化。长期方案是把对话历史写进向量库用检索的方式唤起但短期滑动窗口足够应付大多数场景。6.4 模型照抄垃圾信息幻觉反而更严重这个最讽刺接了搜索幻觉不降反升。原因就是搜索结果里垃圾太多模型又缺乏甄别能力。我总结了一套处理序列域名过滤 → 广告标记过滤 → 摘要截断 → rerank 打分 → prompt 强制优先采用权威来源、冲突时说明。还有一个很实用的 trick在 prompt 里要求模型引用来源 URL 和日期当它被强制给出来源时编造的概率会明显下降因为给出可验证来源这个动作本身就在抑制幻觉。6.5 搜索 API 报错、限流、网络抖动排查清单先看是不是 429限流是就检查本地限流器和缓存是否生效再看超时设置3 到 5 秒比较合理然后看是不是某一个 query 格式异常导致 API 拒绝比如 query 里带特殊符号、长度超过限制在工具解析层做 query 清洗最后看备用源切得够不够快failover 超时设置应该比主源超时更短否则主源卡住时备用源也来不及顶上。再补一个我反复踩坑才总结出来的经验联网搜索做得好的 Agent不是搜得多的 Agent而是知道什么时候不搜的 Agent。很多问题比如解释一下什么是递归模型自己的知识已经完全够用搜索反而会引入噪声只有涉及时效、事实、对比验证时才值得动用搜索。你可以在 prompt 层面对是否搜索做约束也可以按问题类型做路由。我自己现在的习惯是给每个搜索动作都打印一条日志定期复盘搜索触发率如果一类 query 的搜索触发率超过 80% 而回答质量没有明显提升那大概率是工具描述或 prompt 引导出了问题。这个习惯帮我省了不少 token也把回答质量拉了上来。