Agent-Reach:让 AI Agent 联网查证与可追溯引用
2026/9/18 4:15:49 网站建设 项目流程

上个月帮朋友看一个内部知识助手,问题挺典型:问它"我们产品最新版本的接口有没有变动",它能一本正经地给你编出一段看起来很像真的回答,连版本号都编得有模有样。我问他,这助手能不能连到你们自己的文档站和接口变更日志去看一眼?他愣了一下说,没做,一直是拿模型自己脑子里的东西在答。

这就是多数 Agent 落地时最先撞上的墙——模型再强,它也只在一个固定的知识快照上说话。而 Agent-Reach 要解决的,就是把这堵墙拆掉:让 Agent 具备"够得着外部信息"的能力,从搜索、抓取、调接口,一直到把结果干净地喂回上下文并给出可追溯的出处。它不是一个具体产品,更像是一层能力架构,你在做 RAG、做工具调用、做自动化流程时都会碰到它。这篇文章我会按一个真实项目的推进顺序讲:先想清楚 Reach 的边界,再拆开它的内部关卡,接着从零搭一个最小可用版本,最后聊聊实测里最容易翻车的地方和参数怎么调。

1. Agent-Reach 到底解决什么问题:从闭卷考试到带资料进考场

1.1 闭卷 Agent 的能力天花板在哪里

把现在大多数 Agent 想象成一个闭卷考试的学生。它答得好不好,完全取决于两件事:训练时背了多少,以及你的问题是否落在它背过的范围内。凡是涉及"上周刚发的公告""昨天刚改的配置""某个内部系统的实时状态",它就只能靠蒙,而蒙出来的东西还特别像真的,这才是最麻烦的地方——错误答案的可信度太高了。

我在实际项目里踩过最典型的一次坑:客户问"你们的退款政策是不是改了",Agent 从旧版政策的记忆里拼了一段出来,语气笃定,条款编号都对得上,但那个编号在新版里已经被合并了。当天晚上客服主管就把这个功能下线了。事后复盘,问题不在模型,而在于我们把一个需要"查"的问题,交给了只能"回忆"的系统。

闭卷 Agent 的边界其实很清晰:静态知识用推理,动态事实必须靠触达。凡是答案会随时间、随环境、随账号状态变化的内容,都属于 Reach 的管辖范围。分不清这个界线,你会在错误的地方花大量精力去调 Prompt,而正确的地方一点没动。

1.2 Reach 的三层含义:触达、取回、可验证

很多人一提到 Agent 联网,脑子里只有"搜索"这一个动作。真做下来你会发现,Reach 是三层能力叠在一起,缺一层都会让结果变得不可用。

第一层是触达,也就是"知道该去哪儿问"。这不只是搜索引擎的事,还包括内部文档站、数据库、第三方 API、文件系统。一次触达的成败,往往取决于你有没有选对通道,而不是通道快不快。

第二层是取回,也就是把目标位置的东西完整、准确地拿回来。这一层全是工程细节:超时、重试、并发、编码、页面结构变化、接口限流。做得粗糙,触达了也拿不到东西。

第三层是可验证,也就是答案要能对回原始出处。这一层最容易被忽略,但恰恰决定了 Agent 能不能进生产。用户看到"这个版本号来自某某变更日志第 3 条",和你看到一段没有出处的断言,信任度完全不是一个量级。

我在设计自己的模块时,会强制要求每个 Reach 结果都携带sourcefetched_atconfidence三个字段。哪怕一开始用不上,后面排查问题时这三个字段救过我好几次。

1.3 什么场景该上 Reach,什么场景不该上

不是所有 Agent 都需要 Reach,硬上反而会让系统变慢变贵。我一般用两个问题来筛:

  • 答案会不会变?会变就必须上,比如库存、价格、版本、公告、日程。
  • 答错的代价大不大?代价大就必须上,比如医疗建议、金融条款、内部制度解读。

反过来,这两类场景我通常不上:纯创意类任务(写文案、起名字、做头脑风暴),以及答案完全封闭在已有上下文里的任务(总结一段用户刚粘贴的文本)。前者触达了也用不上,后者纯粹是浪费一次调用。

还有一个中间地带值得单独说:看起来静态、其实有版本差异的内容。比如某个开源库的用法,模型记得大方向没错,但参数名在新版本里改了。这种场景上 Reach 的收益极高,因为只需要触达一次官方文档,就能消掉一整类错误。

2. 拆开看 Reach 的内部结构:一次有效触达要经过哪几道关卡

2.1 意图到查询:Query 改写这一关最容易被低估

用户说的是"这个功能还能用吗",传给检索层之前,得先变成能命中目标的一句话。这一转变看起来简单,实际是整个链路里最影响成败的一环。

我见过最多的做法是直接把用户原话扔给搜索接口。结果就是召回一堆八竿子打不着的东西,模型拿到这些垃圾上下文,反而比不联网答得更差——因为它现在有了"依据",会更自信地顺着错误方向走。

比较稳的做法是做两步改写。第一步叫指代消解,把"这个功能""它""那个接口"替换成具体的实体名,这些实体通常来自对话历史或会话上下文。第二步叫查询扩展,把口语化的问法翻译成目标系统更容易命中的关键词组合。举个例子,"退款政策改了吗"可以改写成"退款政策 变更 更新 生效日期",四个词覆盖了文档里可能出现的不同表述。

一个实操细节:改写这步不要用最强的模型,用便宜的小模型跑就够了,但必须给它明确的约束,比如输出限定在一行、不超过 30 个字、不要带问句。我早期没加限制,小模型总爱输出一段解释,直接污染了查询串。

2.2 取回层:并发、超时、重试的取舍

取回层的核心矛盾只有一个:你希望它多快,和它能多稳,通常是反着来的

把超时设得很短,比如 3 秒,慢一点的目标直接被掐掉,链路整体响应很快,但你会漏掉大量本来能拿到的结果。设得很长,比如 30 秒,成功率上去了,但一个卡住的目标会把整轮对话拖到用户失去耐心。

我的经验值是单目标超时 8 秒、整轮触达预算 15 秒,超过预算就带着已有结果往下走。这个数字不是拍出来的,是拿真实目标测出来的:把内部文档站、外部搜索、业务 API 三类的响应时间各采样 200 次,看 P95 落在哪儿,再往上留一点余量。

重试也要克制。我一般只对两类错误重试:连接超时和 5xx,重试 1 次,间隔用 1 秒左右。对 4xx 一律不重试——你参数错了,重试十次还是错的,只是白白烧掉预算。至于并发,多目标场景下用线程池或异步任务并发取,比串行快得多,但并发度别超过目标系统能承受的范围,我通常控制在 4 到 8 之间。

2.3 清洗与切分:把脏数据变成模型能吃的料

原始网页拿回来,里面 80% 的内容是导航栏、页脚、推荐位、广告位和一堆脚本。直接塞进上下文,等于花钱买噪声。

清洗这一步我一般做四件事:剥掉 HTML 标签只留正文文本,去掉连续空行和多余空白,按语义段落切分而不是按固定字数硬切,最后给每一段打上位置标记(比如段落序号和来源标题)。第四件事很多人不做,但它对后面做引用非常关键,没有位置标记你没法告诉用户"答案出自第几段"。

切分长度我倾向 400 到 800 字一段,重叠部分留 10% 左右。太短会切断上下文,太长会让检索精度下降。这里有个反直觉的点:切分粒度是给检索用的,不是给最终阅读用的。检索命中某一段之后,你可以把它的前后邻居一起带上,这样既保住了精度,又保住了连贯性。

2.4 回填与引用:让答案可追溯

最后一步是把处理好的内容放回模型上下文,并且要求它在回答里带上出处。

我的做法是在每段内容前面加一个编号标签,形如[S1][S2],然后在系统提示里明确写:"引用外部信息时必须在句末标注对应编号,没有编号支撑的结论必须说明这是推测。" 这条规则看着朴素,但它把幻觉从一个隐性问题变成了显性问题——模型一旦编内容,往往会忘记编号,用户一眼就能看出来。

顺便说个体感很强的细节:把抓取时间也带上。用户看到"该信息抓取于 14:32",和看到一段没有时间戳的断言,对答案新鲜度的判断完全不同。这在处理价格、库存这类高频变化的信息时尤其重要。

3. 落地实现:从零搭一个最小可用的 Agent-Reach 模块

3.1 目录结构与环境准备

先说清楚,这个最小版本不依赖任何特定平台,你用什么模型、什么搜索通道都能套进去。我按职责分层,目录大概长这样:

agent_reach/ ├── config.py # 超时、并发、条数等参数集中管理 ├── rewrite.py # 查询改写与指代消解 ├── fetchers/ │ ├── web.py # 网页抓取 │ └── api.py # 业务接口调用 ├── clean.py # 清洗、切分、打标 ├── cache.py # 分层缓存 └── orchestrator.py # 编排:改写 -> 并发取回 -> 清洗 -> 回填

参数集中放在config.py里这一条,是踩坑踩出来的。早期我把超时写在各个 fetcher 里,调优的时候改漏了一处,结果线上表现和本地测试完全对不上,排查了两个小时才发现是配置分裂。

环境准备没什么特别的,Python 3.10 以上,requestsbeautifulsoup4lxml三个包基本够用。缓存我一开始用的内存字典,单机跑没问题,多实例部署后立刻失效,后来换成了带 TTL 的共享存储。

3.2 工具注册与描述怎么写,模型才不会乱调

工具描述是 Reach 能不能被正确调用的开关。写得太笼统,模型不知道该用;写得太啰嗦,模型读了半天抓不到重点。

我总结的写法是:一句话说清它干什么,一句话说清什么时候用它,一句话说清什么时候别用。比如:

{ "name": "fetch_page", "description": ( "抓取指定 URL 的网页正文内容。" "当用户问题涉及需要查阅的最新信息、官方文档或具体页面内容时使用。" "如果问题只是对已有上下文的总结或改写,不要调用此工具。" ), "parameters": { "type": "object", "properties": { "url": {"type": "string", "description": "要抓取的完整网址,需包含协议头"}, }, "required": ["url"], }, }

第三个"什么时候别用"看起来多余,实测下来是减少无效调用的关键。没有这句话的时候,模型遇到总结类任务也会顺手调一次抓取,白花钱还拖慢响应。

3.3 取回层的代码骨架

取回层我写成一个统一的执行器,把单个目标的超时和整轮的预算分开控制:

import time from concurrent.futures import ThreadPoolExecutor, as_completed def reach_all(targets, per_timeout=8, total_budget=15): started = time.time() results = [] with ThreadPoolExecutor(max_workers=6) as pool: futures = {pool.submit(fetch_one, t, per_timeout): t for t in targets} for fut in as_completed(futures, timeout=total_budget): target = futures[fut] if time.time() - started > total_budget: break try: results.append({"target": target, "data": fut.result(), "ok": True}) except Exception as exc: results.append({"target": target, "error": str(exc), "ok": False}) return results

这里有个容易被忽略的写法细节:as_completed上也挂了timeout。只给单个请求设超时是不够的,六个目标各 8 秒串起来就是 48 秒,必须有一个总的止损线。另外fetch_one内部要做重试,但重试必须是幂等的、有次数上限的,我一般写成"最多 2 次尝试"。

失败的目标不要直接丢掉,要带着错误信息返回。编排层拿到之后可以决定是降级回答还是提示用户。把失败吞掉不报,是后面最难查的一类问题。

3.4 结果裁剪与上下文预算控制

取回来一堆文本,不可能全塞进去。我给自己定的规则是:所有外部内容加起来不超过总上下文预算的 40%,剩下的留给系统提示、对话历史和输出空间。

具体怎么裁,我用的是一个组合打分:检索相关度占 0.5,来源可信度占 0.2,内容新鲜度占 0.2,长度惩罚占 0.1。分数低的直接砍掉,砍到预算以内为止。长度惩罚这一项很多方案没有,结果是长文档永远挤掉短文档,而短平快的公告往往才是用户真正要的。

去重也是这一步做的。同一件事被三个来源提到,只保留信息量最大的那一段,其余作为"佐证来源"记在元数据里。不这么做,模型会把同一件事复述三遍,回答看起来又臭又长。

3.5 串起来跑第一次

第一版跑通的时候,我只用了一个场景验证:问一个答案明确会变的问题,看它能不能给出正确结果并带上出处。

验证清单我列一下,你可以照着抄:

检查项期望结果
触达是否发生日志里能看到至少一次取回记录
内容是否干净回填文本里没有导航、脚本残留
出处是否标注回答中带编号且编号能对应到具体来源
时间戳是否正确抓取时间与实际时间一致
失败是否可见主动断网测试,错误信息完整返回

这五项里,我建议你优先验证第四项和第五项。时间戳错了说明时区或字段映射有问题,失败不可见说明异常被吞了,这两个问题留到上线后爆发,排查成本会翻好几倍。

4. 实测中最容易翻车的几个点

4.1 工具描述写太全,模型反而不会用

我最开始写工具描述,习惯把能想到的所有场景都列上,洋洋洒洒两百字。结果模型的调用准确率反而下降了。后来才想明白:描述越长,边界越模糊,模型要从一堆并列的场景里自己判断该不该用,判断成本就高了。

改成"一句话触发条件 + 一句话排除条件"之后,无效调用一下子少了差不多一半。这个结论我在三个不同的项目里都验证过,不只是巧合。

4.2 超时设置拍脑袋:一次 10 秒把整条链路拖死

前面提过,我最初统一设了 10 秒超时,理由是"看起来比较保险"。上线之后用户反馈"慢得离谱",一看日志,每轮对话平均要 20 多秒。

问题出在串行调用上:改写一次、取回一次、必要时再补一次取回,每次都可能贴着 10 秒走。三个动作串起来就是 30 秒。改成并发加总预算之后,P95 从 20 多秒降到 6 秒以内。

提示:超时参数不要按"最坏情况多久能返回"来设,要按"用户能等多久"来设,然后反过来优化每层的动作数量。

4.3 把全文塞进上下文:token 账单和噪声同步爆炸

有一次为了"信息更全",我把抓回来的整页正文原样塞了进去,一页大概两万字。结果是:token 消耗翻了三倍,回答质量反而下降了,因为关键信息被淹没在大量无关内容里。

这个坑的本质是把"取回"和"使用"混为一谈了。取回可以宽一些,使用必须窄。现在我的做法是取回保留完整原文存在缓存里,只把裁剪后的片段喂给模型,模型如果需要更多细节,再发起一次针对性取回。

4.4 结果没做去重,同一件事被复述三遍

这个问题特别隐蔽,因为回答不算错,只是很啰嗦。同一份公告被三个来源转载,三段内容高度重合,模型老老实实把三遍都写进了回答里。

修法很简单,在清洗阶段做一次相似度过滤,阈值我设在 0.85 左右。但也别设太低,比如 0.6,那就把不同角度的补充信息也误删了。这个值需要拿你自己的语料调,没有通用解。

5. 参数与策略的调优:把 Reach 从能用推到好用

5.1 并发度与超时的组合计算

这两个参数不能分开调。我给一个我常用的推算思路:假设有 N 个目标,单目标 P95 响应是 T,并发度是 C,那么理论耗时大概是ceil(N / C) * T。你要让这个数字小于整轮预算 B。

拿具体数字代入:N=6,T=3 秒,C=6,算出耗时约 3 秒,远小于 15 秒预算,配置是合理的。如果 N=20,C=4,耗时就是 15 秒,正好卡在预算线上,这时候要么提高并发,要么减少目标数。

注意:并发度提高不是无代价的,目标系统有连接数限制,超过之后错误率会陡增。我一般先按 6 试,观察错误率再上下调。

5.2 检索条数与答案质量的边际效应

检索返回几条最合适?我做过一轮简单的对照测试,结论是条数在 3 到 8 之间收益最明显,超过 10 条之后质量基本不再提升,成本和噪声却持续上升。

具体怎么定,看你的问题类型。事实型问题("某版本什么时候发布")3 到 5 条就够,答案通常集中在一两处。分析型问题("这三家的方案各有什么取舍")需要 6 到 10 条,因为要覆盖多个对象。我现在的默认值设 6,特殊场景在配置里覆盖。

5.3 缓存分层:什么时候该缓存,什么时候必须绕过

缓存能省钱省时间,但用错了会给出过期答案。我分三层处理:

内容类型缓存策略理由
官方文档静态页12 小时变化慢,缓存收益高
公告、变更日志30 分钟变化中等,过期代价可控
价格、库存、状态不缓存或 60 秒过期代价高,宁可重取

第三类千万别缓存,我见过有人为了省钱把库存接口缓存了十分钟,结果用户下单失败率飙升。凡是"用户在页面上直接看到的东西",都要按不缓存处理。

5.4 失败降级的三档策略

触达失败是常态,不是异常,必须设计好怎么退。我一般分三档:

  • 全失败:明确告诉用户"这次没能取到最新信息",然后给出基于已有知识的回答,并标注这是未验证内容。硬撑着装作查到了,是最差的选择。
  • 部分失败:用成功的结果回答,同时在末尾说明哪部分信息没取到。
  • 超时但已有部分结果:直接带结果往下走,不要为了凑齐而继续等。

这三档的判断逻辑我写在编排层,不写在工具里。工具只管失败,编排层决定怎么应对,职责分开之后逻辑清楚很多。

6. 触发时机与成本控制:让 Agent 学会什么时候该出去

6.1 用规则触发还是让模型自己判断

纯规则触发的问题是太死,写死一批关键词,用户换个说法就漏掉了。纯模型判断的问题是太活,简单问题也给你调三次工具。

我最后用的是混合方案:先用规则做一次粗筛,命中"时间敏感词"(最新、今天、现在、有没有变)、"外部实体词"(具体产品名、URL、编号)就直接触发;没命中的交给模型自己判断,但给它的提示里明确写了成本约束,比如"每次触达都消耗资源,仅在你自己无法确定答案时使用"。

这套组合下来,无效触达大概降了六成,而该触达的场景基本没漏。

6.2 复用已有上下文,避免重复触达

多轮对话里最常见的浪费是重复取同一个东西。用户第二轮问"那它的价格呢",如果第一轮已经抓过同一个页面,就应该从缓存或会话上下文里直接拿,而不是重新抓一遍。

我的做法是在会话状态里维护一张"已触达清单",记录每一轮取过哪些源、什么时间取的。下一轮触发前先比对,命中且未过期就直接复用。实现不复杂,但省下的调用量相当可观,尤其是长对话场景。

6.3 给每一次触达打上成本标签

最后说个习惯问题:我要求每一次触达都在日志里记录来源、耗时、token 增量和是否命中缓存。攒上一两周之后,你会很清楚地看到钱花在哪儿了。

我自己跑出来的数据里,最贵的那部分往往不是搜索调用,而是把大段无效文本塞进上下文造成的 token 消耗。有了这个账,优化方向就明确了——先砍上下文,再砍调用次数,顺序反了效果差很多。

这套东西我前后迭代了四五版,最大的体会是:Agent-Reach 的难点从来不在"能不能连上",而在"连上之后拿回来的东西干不干净、能不能查证、值不值这个成本"。每次上线新场景,我都会先跑一轮前面那张验证清单,尤其是失败可见性和时间戳这两项,它们出问题的时候最安静,也最难查。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询