
1. Agent-Reach 到底是什么我为什么要做这个在做 LLM 应用落地的大半年里我有个越来越深的体会模型决定智商Reach 决定能力。简单说一个智能体再聪明如果它只能基于自己训练时的那点记忆回答问题那它就是一座信息孤岛。而 Agent-Reach用一句话概括就是——我给智能体设计的整套“触达能力系统”目标是让它能稳定、高效、安全地触达内部知识库、外部 API、数据库和各类工具把能连的资源全部连起来再把这些能力收拢到一个可控的执行框架里。最开始做这个项目是因为我在实际开发里遇到了一个很典型的场景业务方问“这个月某区域客户的续费趋势怎么样”模型本身完全能理解这个问题但回答不出来因为它看不到客户数据也调不到数据仓库的接口。我当时试过在提示词里塞数据、翻文档、写一次性脚本效果都一般。后来才意识到问题不在模型推理能力上而在“触达半径”上——代理能碰到多少数据源、能调用多少个工具、能不能安全地在这些工具之间做决策。于是我就围绕“Agent-Reach”整理了一整套实践——从工具接入、路由决策、上下文管理到故障排查。这篇博文就是这套实践的完整记录。适合的人群包括正在做 LLM 应用开发但被“工具调用不稳定”折磨的工程师、想评估是否引入智能体方案的产品负责人以及想少踩坑的独立开发者。我不会给你讲一堆停留在概念层面的东西这篇记录里有我用过的代码模板、提示词、踩坑清单和真实案例你可以直接拿去改。2. 整体架构拆解扩展“触达半径”的五个层次2.1 为什么把 Reach 拆成五层来看一开始我做的 Agent 扩展方案非常粗暴把所有工具丢给大模型让它在一次对话里自主决定调用谁。Demo 阶段效果确实惊艳但一到生产环境就露馅。工具一多模型就开始乱选参数一复杂它就开始编响应一超时整个对话就像死了一样。后来我把问题拆开后发现“触达能力”其实不是一个单点问题而是五个层面问题的叠加——我该感知到什么、如何决定干什么、如何执行、如何记住上下文、如何保证安全。分别应对这五类问题整个系统的稳定性和可维护性会好很多。这套架构我清晰定义如下层次核心职责典型组件常见失败模式感知层检索工具结果获取外部信息搜索 API、数据库连接器、爬虫返回内容过多格式化不一致决策层判断调哪个工具、怎么组合路由规则、LLM 工具选择路由错误工具描述混淆执行层真正发起调用处理超时重试HTTP 客户端、执行器循环超时、限流、参数非法记忆层管理短期上下文和长期信息Token 压缩、向量库上下文超长或被截断安全层权限控制、敏感信息审计RBAC、审计日志越权、敏感数据泄露拆完层后我的调试思路就发生了改变以前遇到问题只会怀疑模型现在遇到问题先把故障归到某层——比如日志里看到调用失败我先查执行层的超时配置如果代理连续调错工具我先检查感知层返回的工具描述信息是否足以让模型决策。这种归因习惯真的能节省大量时间。2.2 把分层架构落到实际项目里的边界划分在实际项目中我不建议每个层都做成独立微服务至少初期不要这样。分布式系统里多一个环节就多一层故障和延迟。我在在一个还处于快速迭代阶段的 Agent 系统里一旦把执行层拆成了独立服务结果每次本地调试都要启动三四个进程改一个参数要花半分钟等拉起效率陡降。比较好的做法是初期把五层放在同一个 Python 进程内用模块来分隔职责只对记忆层和工具层做外部依赖抽象。我Agent-Reach 项目在代码层面是这样划分边界的感知层retrievers/目录每个文件封装一种数据源的检索逻辑统一返回结构化 dict。决策层router.py负责判断用户意图并决定调用哪个工具可以是规则匹配也可以交给 LLM。执行层executor.py一个通用的工具调用执行循环处理重试、超时和错误。记忆层memory.py负责截断、摘要和向量召回。安全层日志中间件和接口层权限校验。这样分完以后可测试性也会提高。比如你不想每次都真实调用外部 API可以直接 mock 掉retrievers/里的某个函数模拟大范围工具返回快速验证路由和提示词逻辑。3. 核心选型解析工具调用范式、路由机制和模型参数3.1 原生 Function Calling、MCP 和 ReAct怎么选现在做 Agent 工具调用基本有三条路模型自带的原生 Function Calling、MCP 这类标准化协议、ReAct 这种提示词驱动的隐式调用。我三种都用过一段时间先说说我最终的选择和理由。我当时用了 OpenAI 系列模型和市面上常见的一些开源模型作为对照。原生 Function Calling 体验最好模型输出的工具调用是结构化 JSON不需要自己解析文本命中率也相对稳定。ReAct 虽然灵活、不依赖模型是否支持 function calling但它要求模型自己输出“我要调用 xxx 工具”这样的文本再靠正则解析这种方式的成功率在不同模型间差异很大动不动就解析失败。MCP 解决的是工具定义和连接的标准问题但它本身不是调用范式适合做大范围接入规范。如果你现在要做生产级系统我的建议是优先选原生 Function Calling再配合一套 ReAct 提示词作为不支持该功能的模型的降级预案。具体对比如下方案成功率接入成本结构化程度适用场景原生 Function Calling高低高主力方案ReAct 文本解析中低低兜底方案MCP取决于上层中高多系统标准化接入3.2 路由决策什么时候该用“规则LLM”混合路由路由是整个 Agent-Reach 项目里我最重视的环节。一开始我把路由完全交给模型“自由发挥”结果工具列表超过 15 个后它经常选错。后来我在观察日志时发现很多路由错误不是模型笨而是工具描述写得有歧义。我最后采用的方案是混合路由常见、确定性强的意图走规则匹配模糊或复杂的意图才交给 LLM 判定。这样做的好处是高频路径足够快、足够稳低概率歧义路径再由模型兜底。你可以在router.py里先跑关键词或规则命中不了再去问 LLM而不是把每一轮决策都交给 LLM这样成本更低稳定性更高。一个很典型的例子是如果用户问“查一下订单编号 SO-2024-001 的状态”这明显应该调用订单查询工具不需要经过 LLM 思考但如果用户说“帮我了解一下上季度的业绩情况和我们现在有哪些工具可用”这就要 LLM 来综合判断了。3.3 模型参数和调用策略里容易被忽略的细节关于模型参数如果你做主流程的工具调用温度建议直接设成 0 或 0.1。这里我要特别强调一下为什么工具调用本质是“在给定 schema 和对话历史下填参数”这需要的是确定性不需要创造性。温度调高哪怕一点点都会让模型在参数格式或工具名称上“自由发挥”我非常不建议在生产环境里用高温度跑工具调用。还有一个容易被忽略的是tool_choice。默认情况下模型可以选择不调用任何工具但如果你已经检测到任务强依赖某个工具可以把它固定为必选。我在项目里会保留一个“强制工具模式”——当路由命中后下一轮调用把tool_choice设为特定工具名避免模型拐去答非所问。调用策略上我是单次对话允许多个工具并行调用的。比如感知层同时调用了订单查询和库存查询这两个调用相互没依赖就可以并行执行再汇总结果。但要特别注意并行调用不要超过 3 个太多会让上下文在同一时间塞入大量中间结果给记忆层制造压力。4. 实操实现从零搭建一个最小可用的 Agent-Reach4.1 用代码定义你的第一个工具集合我们先从工具定义开始。在 Function Calling 范式里工具描述比工具实现更重要。描述写得烂模型再强也选不对。我给大家看一个我在 Agent-Reach 里实际使用的工具定义模板TOOLS [ { type: function, function: { name: search_orders, description: ( Search orders by customer name, order ID, date range, or status. Use this tool when the user asks about orders, transactions, or bills. If both customer and date are known, include both. ), parameters: { type: object, properties: { customer_name: { type: string, description: Full or partial customer name., }, order_id: { type: string, description: Exact order ID, e.g. SO-2024-001., }, start_date: { type: string, description: ISO format date, e.g. 2024-01-01., }, status: { type: string, enum: [pending, paid, cancelled], description: Order status filter., }, }, }, }, } ]这个定义里有两个我后来才意识到的关键点。一是description里不仅写了“工具能干什么”还写了“什么情况下该用它”这能极高提升路由命中率。二是参数尽可能用enum做枚举约束模型就很难在参数值上自由发挥。比如状态字段如果不限制枚举它可能给你返回“Paid”“PAID”“paid”各种写法。工具实现本身要对外部系统做好容错。我写了一个通用执行器它不会假设外部接口永远可用def execute_tool(tool_call): tool_name tool_call.function.name args json.loads(tool_call.function.arguments) try: result TOOL_FUNCTIONS[tool_name](**args) return {status: success, name: tool_name, result: result} except TimeoutError: return {status: timeout, name: tool_name, result: None} except Exception as exc: return {status: error, name: tool_name, error: str(exc)}把工具执行结果统一成{status, name, result/error}的结构记忆层拿到这种统一结构后处理起来会轻巧很多。否则十个工具就有十种返回格式压缩和摘要都无从下手。4.2 系统提示词执行循环如何让代理“多轮思考、小步快跑”工具定义好了下一步就是让代理能“循环思考”大模型看到用户问题后如果觉得需要调用工具就输出一个工具调用系统执行工具后把结果交给模型继续推理模型再决定是继续调用新的工具还是直接给用户最终答复。我在 Agent-Reach 里用的核心提示词很简单核心就三句话第一确认你想调什么工具第二看看结果是否是用户想要的第三如果结果不够不要瞎编继续调用工具。但这里我要补充一点别在提示词里写太多戏比如“你是一个乐于助人的AI助手”这类话和调用工具任务没有关系还容易占token预算。工具调用场景下的提示词要像派工单一样简洁。下面是执行循环的最小实现messages [{role: system, content: SYSTEM_PROMPT}, {role: user, content: user_query}] MAX_TURNS 5 for turn in range(MAX_TURNS): response client.chat.completions.create( modelMODEL_NAME, messagesmessages, toolsTOOLS, tool_choiceauto, temperature0.1, ) msg response.choices[0].message messages.append(msg) if not msg.tool_calls: break for tool_call in msg.tool_calls: tool_result execute_tool(tool_call) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(tool_result, ensure_asciiFalse), }) final_answer messages[-1].content这里有个非常关键的设计MAX_TURNS 5。为什么要设上限因为模型在没有足够信息时有可能陷入“调用工具-结果不理想-再调用工具”的死循环。这个上限一方面保护你的 API 预算另一方面强迫系统在有限步骤内收敛。我实际的经验是60% 的请求 2 轮内就能解决30% 需要 3-4 轮超过 5 轮的请求往往是路由选错了工具或参数传入方向错了这种时候继续循环只会浪费钱。4.3 重试、超时和回退给代理穿上“防弹衣”外部接口调用要建立“失败也优雅”的机制。我在 executor 里给所有工具调用统一配了 15 秒超时、2 次重试的默认规则。为什么是 15 秒因为大模型本身推理消耗已经不小了如果工具调用再花 30 秒用户体验完全没法接受。再就是回退策略。具体项目里我建立了一个“回退工具链”的概念比如主检索工具挂了可以让代理切到搜索引擎接口甚至直接返回静态说明文档内容。实现方式也很简单就是在工具函数里做依赖注入def search_orders_with_fallback(order_id): try: return query_order_db(order_id) except ConnectionError: return query_order_api_v2(order_id)很多工具链的可用性可以说就是靠这些回退逻辑扛起来的。你也不需要所有工具都做回退优先给高频工具和核心链路做就够了。5. 上下文与记忆策略触达范围扩大之后的信息管理5.1 Token 预算模型你的工具结果不能无限制塞进上下文工具调用越多token 消耗越恐怖。我刚开始接入一堆工具时经常出现一种情况代理明明已经拿到了结果却因为上下文里塞满了工具返回的原始 JSON后续推理变得非常迟钝甚至直接“失忆”。我把这个问题命名成“触达膨胀”——Reach 扩大了信息变多但模型注意力是有限的。对此研究者最好的策略是给整个系统设定一套 token 预算。我的分配比例如下系统提示词和工具定义约 2,000 token最近 2 轮对话历史约 1,500 token当前工具返回结果约 1,500 token长期记忆摘要约 1,000 token这套预算是动态压缩的结果。如果工具返回结果超过 1,500 token我不会全量塞给模型而是先在感知层做一次“结果摘要”只把关键字段和聚合数据传给下一步。5.2 三种工具结果的压缩玩法截断、摘要、结构化提炼具体实现上我按工具结果的特点分成三种处理方式列表型结果比如搜索返回了 50 条记录我会先按相关性排序截取前 10 条并附上“共命中 50 条此为前 10 条”的说明。长文本结果比如文档内容调用 LLM 做单次摘要提取结论、关键数字和下一步建议。多工具汇总把各个工具的结果合并成一份“数据简报”避免模型在多次切换工具后忘记上下文。这三种方式里最容易被忽略的是列表型结果的截断。你实际处理时会发现模型不需要 50 条记录它只需要看到最相关的几条就够了。告诉它“一共 50 条只看前 10 条”它在做总结时是完全可以胜任的。5.3 长期记忆跨对话记住用户的偏好和项目背景Agent-Reach 如果只支持单轮对话那它只是一把“长柄镰刀”还算不上“长臂猿”。生产中你肯定希望 Agent 能跨对话记住信息上次查询的客户归属、用户偏好的报告格式、之前处理过的工单状态。我在项目里用的是“摘要向量召回”的两层记忆。每轮对话结束后把重要信息生成一条摘要写入向量库新对话开始时按当前问题召回相关摘要拼接进上下文。这里的一个小技巧是摘要必须按“键值对”组织比如偏好周报形式不要把散文写进去这样召回效率和后续提示词拼接效果都会更好。注意长期记忆涉及用户隐私。建议至少做到“用户级隔离”即用户 A 无法召回用户 B 的记忆碎片这在多租户系统里是一条红线。6. 常见问题与排查技巧实录6.1 智能体选错工具或参数怎么定位这是 Agent 开发里碰到的第一大问题。排查时先别急着骂模型。我建议按这个顺序来查看路由层日志——命中了哪个工具。经常你会发现需求是查订单代理去查了客户信息这是路由阶段就错了。检查工具描述是否覆盖了该意图的关键词。我用过一个案例“查发票”这个意图长期路由到“查订单”工具后来我给工具描述里加了“发票、开票、报销、税号”这几个关键词后问题立刻消失。检查参数抽取——确认是否为必填参数是否在描述中给出了示例格式。日期参数经常出错所以我习惯于给出“ISO format date, e.g. 2024-01-01”这样的示例。排查时养成看“思考日志”的习惯。正规化这步叫“思维过程追踪”简单做法就是每轮循环时记录模型输出的 debug 信息哪怕是 tool_calls 的名称、参数、返回状态这三样东西串在一起基本就能还原出代理每一轮的决定脉络。6.2 上下文超长导致的“失忆”和幻觉Agent 在对话超过十轮后经常出现前后矛盾前面说自己要查 7 月的订单后面又说 6 月。这种问题的根源往往不是模型真的“忘记了”而是对话历史被整体塞进上下文后模型对后续内容的注意力权重被稀释了。我的解决办法分两层。第一层是老生常谈的“压缩对话历史”每两轮生成一个阶段性总结丢弃原始细节。第二层是我觉得更有用的做法——“状态显式追踪”把任务核心状态单独抽出来每次循环都前置显示。比如当前子任务状态 - 数据源已调用订单库获取 7 月订单 38 条 - 待办计算复购率并与去年同期对比 - 下一动作调用标签统计工具这段文字每次循环都放在用户消息的最前面模型再也不会“忘事”。本质上是给模型构建一个“任务清单工作台”这比让它从对话历史里自己归纳要稳得多。6.3 外部接口限流/超时的应对策略接入第三方 API 时限流是家常便饭。我在 Agent-Reach 里做了一个简单的“限流符号”机制RATE_LIMIT_FLAGS { orders_api: {max_calls_per_min: 20}, } def check_rate_limit(tool_name): ... if call_count RATE_LIMIT_FLAGS[tool_name][max_calls_per_min]: return {status: rate_limited, message: Rate limit reached. Wait or use fallback.}一旦工具返回rate_limitedexecutor 不会盲目重试而是直接进入回退链路或告诉代理“该接口忙换个工具实现同样目的”。这比反复调用浪费配额然后等 429 好得多。超时方面还会有一个反直觉的细节模型调用工具后等待时间如果太长它会自动认为工具“应该有结果”然后开始生成臆想内容。所以哪怕没有结果也要返回带超时标志的空结果并在日志里明确记录。空手而归比“瞎编结果”好一万倍。6.4 排查速查表症状优先排查点常见修复用了错误的工具工具描述、路由日志增加关键词、重写描述参数错得离谱参数约束、示例格式增加 enum、给出格式示例调用成功但结果未用上压缩策略、返回结构统一结果 JSON前置状态摘要循环调用停不下来MAX_TURNS 和执行日志降低轮次上限检查路由逻辑接口超时超时配置、网络链路15 秒超时配置回退工具7. 我在实践中反复打磨的三个体会Agent-Reach 这套东西从上线到现在整体把任务成功率从最初的 71% 提升到了大约 94%API 调用成本反而有所下降。为什么成本下降了?因为路由命中率高了之后模型不需要靠“试错”来完成任务少调了好多无效工具。如果让我总结核心经验三个词可以说完结构化、确定性、留退路。结构化指的是工具定义、返回格式、日志结构都要规范化确定性指的是高频路径尽量用规则而不是模型留退路指的是每一个工具调用都要有回退方案哪怕只是返回一句“该接口暂不可用”。最后分享一个很多人都会忽略的技巧你的系统提示词里每次迭代都应该重新审查。工具更新了旧提示词里的工具名可能已经不存在了但模型还在那里期待调用它。每半个月批量跑一遍历史测试集成本不高却能避免非常多的低级事故。如果你也要做智能体触达能力扩展建议从最小的两个工具开始把架构搭稳再接更多工具。再加第三个工具时你会发现真正难的已经不再是模型接口而是你的工具定义和执行逻辑能不能跟上。