如果你搞过 AI Agent 项目,大概率见过这种名场面:演示环境里模型口若悬河,计划列得头头是道,一旦要接真实系统,它就站在原地干瞪眼。你说“查一下订单状态”,它回复“好的,正在为您查询”,然后就没有然后了。这不是模型不够聪明,而是它缺一双“够得着真实世界的手”。Agent-Reach 这个名字,拆开就是 Agent 与 Reach——智能体与触达能力,我把它做成了一层专门解决“意图到动作”问题的连接层。你可以把它理解成 Agent 的通信总机:模型负责想,Reach 负责干,所有工具接入、参数校验、权限控制、结果回传都在这一层完成。这篇文章是项目完整复盘,包含架构设计、工具接入细节、翻车实录和成本控制方案,适合正在做 Agent 应用、被工具调用折磨过的工程师,也适合刚接触 Function Calling 想搞懂底层机制的朋友。
1. 为什么需要一个“触达层”:Agent不是会说话就能干活
1.1 从对话闭环到行动闭环,差的不是模型而是“手”
大模型本质上是一个文字生成器。给它一段输入,它预测下一段最合理的 token 序列。ChatGPT 刚火那阵,大家惊叹的是它“会说话”;到了 2024 年以后,行业里真正值钱的东西变成了“会做事”。一个 Agent 要能做事,必须形成一个完整的行动闭环:感知用户输入,形成意图,调用某个系统接口,拿到真实结果,再基于结果继续推理。这个闭环里,模型只负责“感知”和“推理”这两段,剩下的一大半——意图到具体 API 调用的翻译、参数的格式化、调用的可靠执行、结果的压缩回传——都需要一个层来兜底。
我把这个层命名为 Agent-Reach。Reach 在英文里有“够得着、覆盖面”的意思。Agent 想查天气,它自己不会发 HTTP 请求;Agent 想订会议室,它自己不知道会议室系统的鉴权方式。这些“手伸不到的地方”,就是 Reach 层存在的价值。它不参与模型的思考,只负责把模型“想做的事”变成“能做到的事”。
1.2 我的第一个失败原型:模型很聪明,系统很茫然
最早我做的客服助手原型,逻辑非常简单:让模型先判断用户意图是“查订单”“退换货”还是“开发票”,然后代码里写一个巨大的 if/else 分支,按意图去调不同函数。Demo 跑通那天我很兴奋,但上了点复杂度就崩了。
第一个问题,意图分类太粗。用户说“我上周买的东西还没到,能不能帮我看看怎么回事”,模型给出的意图是“查订单”,但“查订单”背后其实要查订单状态、查物流轨迹、查预计到达时间三套不同系统。我把三套逻辑塞进一个函数里,代码很快就变成一团乱麻。第二个问题,加新功能要动主流程。第三周产品说“加一个运费险理赔入口”,我得在这个 if/else 大锅里再添一个分支,改完还要重新测一遍所有旧意图,生怕把别的流程弄坏。第三个问题最致命,模型输出了幻觉参数——它把用户提到的“8月12号”猜成了订单号传给了查询函数,系统直接抛异常,用户看到的是客服机器人“服务开小差”。
这三个问题指向同一个根因:模型和系统之间缺少一个能“翻译、校验、路由、执行”的中间层。我要是继续在业务代码里堆 if/else,迟早被自己埋的雷炸死。
1.3 Agent-Reach 的定位:替模型把“想做的事”变成“能做的事”
所以 Agent-Reach 从一开始就给自己划清了边界:不推理,不决策,只连接。它要负责五件事。一是工具注册,让所有能被 Agent 调用的系统能力都以统一格式登记在册;二是意图路由,把模型给出的工具调用意图映射到具体的注册条目上;三是参数校验,模型生成的参数必须过一遍硬校验才能放行;四是执行与容错,真实调用背后可能是 HTTP 接口、数据库、甚至是另一个 AI 服务,这一层要统一处理超时、重试和异常;五是结果回传,把工具返回的原始结果压缩、格式化后再喂给模型,让模型能基于真实数据继续完成对话。
这么说吧,Agent-Reach 在 Agent 系统里的角色,像公司前台的总机。各个业务系统是分机,模型是打电话的人,总机负责把电话接到正确的分机上,通话过程做好录音(日志),遇到占线还要告知来电方“请稍后再试”。没有总机,每个分机都得自己接客户电话,早晚乱套。
2. 核心架构:把“手脚”做成可插拔的插槽
2.1 五段式请求流水线:模型只管想,Reach 负责干
Agent-Reach 跑在主模型和真实系统之间,整个请求经过五个阶段。
第一阶段是意图接收。模型在推理过程中决定要调用某个工具,输出一段结构化指令,最常见的形式是{"action": "query_order", "params": {"order_id": "12345678"}}。不管上游是 OpenAI 的 Function Calling、Anthropic 的 Tool Use,还是自己训练的模型输出规范,Reach 层都把它统一成一个内部标准结构:工具名加参数字典。
第二阶段是路由解析。拿到工具名之后,Reach 层先在注册表里精确查找。命中就直接进入执行,未命中就做模糊匹配,看看模型是不是把工具名写错了。这一步还负责判断工具在当前上下文里是否可用——权限不够、已被下线的工具,在这一步就会被拦下。
第三阶段是参数校验。注册表里每个工具都有一份 JSON Schema,校验器会把模型给的参数逐字段核对:类型对不对、必填项齐不齐、枚举值是否合法、格式是否符合正则。校验失败的请求会被打回,但打回的提示信息必须“可修正”,不能是冷冰冰的报错。
第四阶段是执行与容错。Reach 层把参数展开成真实调用,包上超时控制、重试策略和异常捕获。调用成功之后,原始返回值还不能直接丢给模型——很多接口返回的 JSON 动辄几十 KB,模型看了也消化不了,必须先压缩。
第五阶段是结果回传。压缩后的结果、执行状态、耗时和错误码一起组装成一条“工具执行报告”,交还给模型。模型基于这份报告继续推理,生成最终的用户回复。
这五个阶段都收敛在 Reach 层内部,业务方接入一个新工具,只需要“登记一下工具定义,写一个处理函数”,主流程一个字符都不用改。
2.2 工具注册表:一个描述文件加一段处理函数
工具注册表是整个 Reach 层的核心数据结构。我给每个工具设计的登记字段如下:
| 字段 | 说明 | 示例 |
|---|---|---|
| name | 工具唯一标识,小写下划线风格 | query_order |
| description | 给模型读的功能说明,写清触发条件和参数含义 | 见下文示例 |
| parameters | JSON Schema,定义参数的类型、必填、枚举、格式 | 标准 JSON Schema 对象 |
| handler | 实际执行逻辑的函数入口 | query_order_from_db |
| permission | 权限等级:readonly / user_confirm / admin | readonly |
| timeout | 单次调用超时秒数 | 5 |
| retryable | 是否允许自动重试,幂等操作才能为 true | false |
name 和 handler 是工程上用的,机器能懂就行。真正要花心思打磨的是 description 和 parameters,因为这两个字段是给模型“看”的。模型读懂了,调用成功率就高;读不懂,后面所有校验和执行都是白搭。
这套设计最大的好处是“可插拔”。新工具接入成本被压到最低:写一个描述、挂一个函数、登记进注册表,三步完成。哪天某个系统下线了,把对应工具从注册表摘掉,所有 Agent 立刻失去对该系统的触达能力,不会有人再去引用一段已经废弃的函数。
2.3 为什么我不把所有工具都塞进 System Prompt
有人会问:模型平台不是原生支持 Function Calling 吗?把工具列表直接传给模型不就行了,何必自己搞一套注册表?这问题我确实认真想过,最后还是没有选择“全量塞进上下文”的方案。
原因有三。第一,上下文窗口没有你以为的那么宽。工具描述写得详细一点,一个工具轻松吃掉 300~500 token,挂了 30 个工具,每轮对话光是工具描述就一万多 token 没了,上下文很快被撑爆,留给对话历史的余量微乎其微。第二,工具越多,模型越容易选错。给模型 50 个工具,它的注意力会被稀释,试过就知道,工具数超过 15 个以后,选择准确率明显下降。第三,安全不可控。全量暴露意味着所有 Agent 都能看到所有工具,只读工具和资金操作工具混在一页说明书里,模型一糊涂就可能拉起高危操作。
Reach 层的解法是“按需装载”:先用关键词或向量检索做一次窄路由,只把与当前任务最相关的那 3~5 个工具描述注入模型。工具再多,模型每次看到的都只有一小撮,准确率和成本都好看很多。
3. 工具接入的三个关键细节:描述、校验、路由
3.1 工具描述是写给模型看的需求文档
我在项目里改得最多的,不是执行代码,而是工具描述。同一套接口,描述写得烂和写得好,模型调用成功率能差出一大截。
举个对比。一个订单查询工具,烂描述长这样:“查询订单状态。参数:订单号。”这等于什么都没写。模型不知道什么时候该用这个工具,不知道参数格式,遇到“8 位纯数字”以外的输入只能瞎猜。
好一点的描述是:“根据订单号查询订单的当前状态。当用户询问‘我的订单到哪了’‘发货了吗’‘物流到哪了’时必须先调用此工具。参数 order_id 为订单号,通常是 8 位纯数字;如果用户只提供了 6 位数字的订单号,请先向用户确认完整的 8 位订单号,不要自行猜测补全。”
注意几个细节:我写了明确的触发条件——“当用户询问……”;写了参数格式和边界——“通常是 8 位纯数字”;写了冲突时的处理原则——“先确认,不要猜测”。模型是概率模型,你给它越明确的边界,它越能稳定落在边界内。这个技巧后来被我复制到所有工具上,调用的幻觉率降了一半以上。
3.2 参数校验:永远不要相信模型生成的参数
模型生成的参数,看起来很合理,实际上经常出幺蛾子。类型错乱是家常便饭,字符串和数字混淆、日期格式五花八门、枚举值拼错大小写。更危险的是“看起来合法”的错误参数——比如用户说“帮我查 8 月 12 号的订单”,模型可能把“8 月 12 号”这个日期填进 order_id 里,格式校验根本拦不住,因为日期字符串本身也是合法字符串。
Reach 层对参数校验给了一套硬规矩:先用 jsonschema 库做格式校验,拦下类型和必填错误;再对业务关键字段做语义校验,比如订单号必须匹配特定的正则;最后在 handler 里再写一层防御性检查,逐字段验证取值范围。三层校验下来,能挡住绝大多数脏参数。
校验失败后,错误信息不能直接丢给模型。给模型看的错误要“能指导修正”,比如“order_id 字段需要 8 位纯数字,当前收到的是 8 位字符串但包含字母,请重新提取用户提到的订单号”。这种信息模型读得懂,能自己纠正重试;而像“ValidationError at field order_id”这种信息,模型只会一头雾水,然后胡乱重试。
3.3 路由逻辑:从规则匹配到语义匹配的演进
路由层负责回答一个问题:模型输出的这个工具名,到底该映射到注册表里的哪个工具?早期版本我用纯字符串匹配,工具名必须是精确名称,比如模型输出query_order,注册表恰好有这个名字,就命中。但模型偶尔会层叠式地输出query_order_info、query_order_status这种“看起来相似但完全不存在”的名字,字符串匹配直接落空。
后来我加了一层语义匹配兜底:先做精确匹配,失败后用编辑距离或向量相似度检索注册表,找出最接近的工具。相似度超过阈值的,自动纠正并打一条日志;没超过阈值的,把可用的工具列表精简后回传给模型,让模型自己重新选择。
还有一种情况是模型把不同层的工具名张冠李戴,比如让人脸识别工具去处理订单问题。这种语义错误靠匹配救不回来,得靠工具描述里的“触发条件”来约束——这也是我坚持在 description 里写清楚“什么任务归我管,什么任务不归我管”的原因。路由层不是万能的,真正兜底的永远是清晰度的设计。
3.4 一个可运行的骨架代码
下面是一段精简版的 Reach 层核心逻辑,我把路由、校验、执行、回传串在一起,方便理解整体流程。
# reach/executor.py import jsonschema from .registry import TOOL_REGISTRY def execute_tool(tool_name: str, params: dict) -> dict: tool = TOOL_REGISTRY.get(tool_name) if not tool: return {"ok": False, "error": "tool_not_found", "tool": tool_name} # 1. 参数校验 try: jsonschema.validate(params, tool["parameters"]) except jsonschema.ValidationError as e: return {"ok": False, "error": "invalid_params", "detail": e.message} # 2. 执行与容错 try: result = tool["handler"](**params) except TimeoutError: return {"ok": False, "error": "timeout"} except Exception as e: return {"ok": False, "error": f"{type(e).__name__}"} # 3. 结果压缩后回传 return {"ok": True, "result": simplify_result(result)}这段代码很短,但它体现了 Reach 层的骨架:任何进入执行器的调用,必须依次经过存在性检查、参数校验、执行、简化回传。我在真实项目里还给它加了耗时统计和结构化日志,思路完全一样,只是多了几个装饰器。有一点要提醒:校验器的错误信息要先转成自然语言再返回,别直接把jsonschema的原始报错塞给模型,不然它只会原地打转。
4. 最容易翻车的四个坑:幻觉、超时、死循环与权限
4.1 幻觉调用:模型编造出不存在的工具名
上线第一周,日志里出现一条诡异的记录:工具名send_email_v2,注册表里根本没有这个工具。查了上下文才知道,用户问“把发票发到我邮箱”,模型在推理中脑补出一个“更合适的工具”,自己起了个send_email_v2的名字就调了。
排查链路很清楚:Reach 层第一步查注册表,未命中,返回tool_not_found,连带把失败原因记进了日志。但问题是,模型收到tool_not_found之后有两种反应,一种是重新选择正确的工具,另一种是更固执地换个名字再试一次。我见过最夸张的一次,模型在 4 轮对话里编了 5 个不存在的工具名,活像一个撒谎不打草稿的小孩。
修复方案分两层。第一层在路由层:增加模糊匹配,当用户请求和某个工具的语义极度接近时,自动纠正并提示“你可能想调用的是 query_invoice_email 而不是 send_email_v2”。第二层在回传信息里:明确告诉模型当前可用工具的精简列表,并强调“只能从上述列表中选择,不要自己发明新工具”。两层配合,幻觉率显著下降。现在回看,根子还是在工具命名和描述上,越符合直觉的命名,模型越不容易跑偏。
4.2 超时与重试:默认重试三次会变成灾难
另一个让我印象深刻的坑来自“默认重试三次”这个想当然的设计。某个订单查询接口,高峰期平均响应 3 秒,个别请求会拖到 30 秒。早期我在 Reach 层给所有工具统一加了重试机制,失败就重试,最多 3 次。结果某次线上故障期间,接口持续超时,一个用户请求进来,光是死等重试就耗了 90 秒。用户早就走了,Reach 层还在傻等第三次重试。
后来我把重试策略彻底改了。第一,每个工具在注册表里显式声明retryable字段,只有幂等操作才允许重试。像“查询订单”这类只读操作可以重试,但“发起退款”“发送短信”这类非幂等操作,重试一次都可能造成重复扣款、重复发送,绝对禁止自动重试。第二,超时分级。只读接口最多等 5 秒,写操作最多等 10 秒,超过直接快速失败,把“工具超时”作为明确信号回传给模型。模型收到超时信号后会选择等待后重试或更换策略,好过让整个链路干等。第三,加了熔断逻辑。同一个工具连续失败 5 次,自动进入冷却期,5 分钟内不再执行真实调用,直接返回“服务暂不可用”给模型。这招在预防“下游故障传导给 Agent”时特别好用。
4.3 死循环:同一个失败工具被反复调用
重试改完,又遇到一种更隐蔽的问题:模型自己陷入了循环调用。现象是这样的——某个工具因为下游服务 503 返回失败,模型读到了错误信息,决定再试一次;再失败,又试一次。日志显示 10 分钟内同一个工具被调用了 17 次,每次都以失败告终,Token 烧掉一大把,用户的问题完全没解决。这属于“模型的固执”和“系统的真实状态”脱节,模型没有感知到整个链路已经处于故障状态。
我的对策是在 Reach 层加两个硬性限制。第一,单轮对话内,每个工具最多执行 N 次(我设的是 4 次),超过限制,后续同工具调用直接返回特殊信号:“该工具在本轮对话中已连续失败 X 次,建议放弃该方案、更换其他工具,或请用户稍后再试”。这个信号模型能够理解,不再执着。第二,增加“连续失败指数”,当同一个工具在同一轮里连续失败超过 2 次,Reach 层会主动降低该工具在上下文里的可见性,比如从工具列表里暂时隐藏,倒逼模型换路。设置这两条之后,死循环基本绝迹。
4.4 权限边界:危险操作必须有人工闸门
权限设计是 Reach 层最敏感的部分。最开始我只做了一级管控:所有工具调用都记录日志,但都自动放行。结果某次测试里,模型在没有任何显式授权的情况下,调用了一个“批量发送营销短信”的工具,虽然内容无害,但把我吓了一跳——这只是测试环境,如果是生产环境,一次误调用就是事故。
后来我引入了三级权限模型。readonly级别的工具,比如查天气、查订单状态,模型可以直接调用;user_confirm级别的工具,比如发起退款、发送邮件,必须先把审批请求弹给用户,用户点了确认才真正执行;admin级别的工具,比如批量删除数据、修改系统配置,需要管理员二次审批。每一级都写死在工具注册表的permission字段里,路由层在执行前强制检查。
这个设计其实借鉴了支付系统的思路:越危险的操作,越要人工介入。模型再聪明,也只是概率分布,永远存在小概率的“理解偏差”,人工闸门能把这部分风险挡住。我后来审计过一轮,发现所有事故级别的异常,几乎都发生在没有人工闸门的功能上,可见这条规矩有多重要。
5. 从单机到协作:Reach 层如何支撑多 Agent 复杂场景
5.1 把工具升级成可组合的“技能”
单工具运行顺畅之后,我开始琢磨上层复用。用户的需求通常不是“查一下天气”这种单一动作,而是“根据天气安排这周的户外活动”这种复合动作。直接把复合动作写成一个巨大的工具,违背了 Reach 层“小而清晰”的初衷,维护性会很差。
于是我引入了一层“技能”的概念:技能 = 多个工具 + 编排规则 + 前置条件。比如“安排户外会议”这个技能,底层由query_weather、query_calendar、book_meeting_room三个工具组成,编排规则是“先查天气,再查日历,最后根据天气和空闲情况预约”,前置条件是“必须满足当天无已有会议且天气允许户外”。Agent 调用的是技能,Reach 层负责把技能展开成工具序列,逐个执行。技能与工具分层管理,好处是可复用——天气工具既可以被“户外会议技能”用,也可以被“出行建议技能”用,不需要重复实现。
5.2 多 Agent 协作中的呼叫转移与统一治理
复杂场景下,单个 Agent 会力不从心。用户问“这周三是晴天吗?如果是我想约客户下午茶,顺便看看附近有没有合适的咖啡馆”,这个问题横跨天气、日历、地理信息三个领域。我用多 Agent 架构来解:主 Agent 负责任务拆解,三个专项 Agent 分别负责天气、日程、地点推荐,每个专项 Agent 都通过 Reach 层调用各自的工具。
这带来了新的治理挑战。多个 Agent 以前各自为政,每个都自己接系统,日志格式不统一、鉴权方式杂乱、失败的锅互相甩。Reach 层在这里的价值就体现出来了:所有 Agent 的工具调用都走同一个入口,统一鉴权、统一日志、统一限流。出了问题,我可以从 trace_id 出发,把“主 Agent 做了什么决策、哪个子 Agent 调了什么工具、哪个环节拖慢了”完整还原出来。这种可追溯性,在多 Agent 场景里不是可选项,而是刚需。
5.3 一个端到端链路:从自然语言到跨系统任务办结
拿一个真实场景串一遍。用户说:“帮我查一下周五下午三点到四点的天气,如果天气好就订一间室外会议室,天气不好就订室内。”主 Agent 先拆任务,然后天气 Agent 调用 Reach 层的query_weather,参数是城市和时间段;Reach 层校验参数、执行接口、把“多云,24 度,降水概率 20%,建议可户外”压缩后回传;主 Agent 接着让日历 Agent 调用query_calendar_availability查空闲时段;拿到空闲结果后,预订 Agent 调用book_meeting_room,权限等级是user_confirm,Reach 层先把预约请求弹给用户确认;用户点了同意,工具才真正执行,返回会议室预定成功。
整条链路里,模型只负责拆解和决策,真正的系统调用、参数校验、权限确认、结果压缩全部发生在 Reach 层。每一段动作都有日志,用户确认过的操作可以被审计。这种“你负责想,我负责稳”的分工,把复杂系统的风险收敛在一个可控的层里,是我认为 Agent 落地最该有的形态。
6. 可观测性与成本控制:别让 Agent 悄悄偷走预算
6.1 每次工具调用都要有据可查
Agent 类产品最让人头疼的问题之一,是“黑盒感”——模型到底调了哪些工具?为什么用户问了个简单问题,Token 消耗却异常高?如果没有日志,这些问题只能靠猜。Reach 层从一开始就把可观测性当硬指标,每一次工具调用都会落一条结构化日志,如下表所示:
| 字段 | 示例值 | 用途 |
|---|---|---|
| trace_id | tr_7f3a9c | 贯穿一次用户会话 |
| tool_name | query_weather | 记录实际调用的工具 |
| params_hash | a3f9b2 | 参数哈希,避免敏感信息明文落库 |
| latency_ms | 320 | 定位慢接口 |
| token_cost | 1240 | 成本归因 |
| status | ok | 区分成功、超时、校验失败 |
| error_detail | tool_timeout | 失败时记录错误码 |
有了这些日志,排查问题的姿势完全变了。以前是“用户上报异常,我对着代码猜”,现在是“按 trace_id 拉出整条链路,一眼看到是哪一步卡住”。成本归因也简单了,某个 Agent 特别烧钱,拉出它的工具调用明细,一眼就能找到烧钱大户。
6.2 Token 开销的隐形放大器:别把完整结果塞回上下文
工具返回结果直接回传,是成本失控的第一大元凶。我见过一个电商 Agent,订单查询接口返回的 JSON 包含几十个字段、几百行明细,一次调用回传一万多 token。模型本来只需回答“您的订单已发货,预计后天到达”,却被迫把整份订单详情所有字段都读一遍,多烧了很多钱。
Reach 层的处理策略是“摘要优先,按需取详”。默认情况下,工具结果先经过simplify_result压缩到 500 字以内,只保留模型回复用户真正需要的几个字段。代码如下:
def simplify_result(result, max_chars=500): text = json.dumps(result, ensure_ascii=False) if len(text) <= max_chars: return text return text[:max_chars] + "...(truncated)"简单粗暴,但效果立竿见影。更进一步,我会对常用工具写定制的摘要器:比如订单工具只保留“状态、预计送达时间、承运商、运单号”四个字段;天气工具只保留“天气、温度范围、降水概率、建议”四个维度。定制摘要能控制在 200 token 以内,比通用截断质量更高。设置这套机制后,工具回传平均 Token 量下降了约八成。
6.3 缓存、摘要与引用式返回:成本优化三板斧
除了压缩结果,Reach 层还有三个优化手段。第一是缓存。对于低时效的工具,比如天气查询、汇率查询,加一层带过期时间的本地缓存,同一参数在 10 分钟内直接命中缓存,不触发真实调用。天气这种数据,一分钟前查和现在查结果不会有任何变化,完全没有必要反复打接口。
第二是引用式返回。如果某个工具的返回结果特别大,模型又确实不需要逐字读取,我会让 Reach 层把完整结果存入临时存储,回传给模型的只是一个引用 ID,比如“查询结果已保存为 #a3f9,如需要更多详情再查询”。模型知道这个 ID 存在即可,不必把大对象塞进上下文。
第三是按需分页。列表类数据,比如“查询用户的所有订单”,默认只回传前 5 条,并提示“如需查看更多,可以调用list_more_orders工具翻页”。大多数用户只关心最近几单,模型根本不需要读完整列表。
做了这三板斧之后,单次对话的平均 Token 开销大概降到了优化前的三分之一到四分之一,用户体验反而更好了——回复更快,因为模型不需要处理那么长的输入;成本更低,因为输出也更短。这些优化直接干到了 Agent 应用的利润表上。
7. 如果你也想搭一个 Reach 层,我的个人建议
7.1 先定义工具边界,再写第一行代码
很多团队一上来就急着调框架、选模型,工具清单却想都没想。结果做到一半,发现某个关键工具没接入,整个链路缺一块,又回头补需求,反复横跳。我的建议是,动手写代码前,先列一张“工具边界清单”:这个 Agent 到底能碰哪些系统?哪些操作是只读的?哪些操作必须人工确认?哪些操作是绝对不允许的?清单不用很长,三五条就够,但一定要写清楚。
我自己做 Agent-Reach 的第一版,只接了三个只读工具:查订单、查天气、查日历。就这么点儿工具,已经把描述设计、参数校验、权限分级、日志追踪这套链路完整跑通了。边界清晰的好处是,后面加新工具时,心里有一张现成的“准入标准”对照着看,不会什么需求都往里引。
7.2 技术栈与迭代节奏:别一上来就上分布式
技术选型我的建议是越简单越好。Python 加jsonschema,再加一个轻量的日志存储,完全够第一版用。很多人一听说“Agent 系统”就想到 FastAPI、Docker、Kubernetes、向量数据库,其实第一版根本用不上这些。Reach 层本质上是一个“校验加转发”的服务,单机进程足够扛住开发阶段的所有流量。
迭代节奏可以这样安排。第一版,只做工具注册表、路由、校验、执行和压缩回传,目标是用三个只读工具跑通一个完整对话闭环。第二版,加权限等级和人工确认机制,把危险操作管起来。第三版,加缓存和成本优化,把 Token 开销压下来。第四版再考虑多 Agent 协作和更复杂的编排逻辑。每一步都有明确的验证目标,出了问题也知道在哪儿找,比憋大招靠谱得多。
7.3 两件让我少走弯路的小事
最后分享两个踩过坑之后总结的细节。第一个是给工具的 description 多花十分钟写清楚,真的能省后面十个小时的调试。反例我都懒得数了,描述写得含糊,模型调用错误率直接翻倍,排查半天还以为是路由逻辑的问题。现在我的规矩是,每个新工具上线前,必须拿三个不同类型的用户问题去测,确认模型在合理场景下都能找到正确的工具、填对参数规格。
第二个是别把 Reach 层做成“万能层”。我见过有人想把业务规则、数据转换、甚至部分对话逻辑全塞进这层,结果它膨胀成一个大泥球,比最初的 if/else 还难维护。Reach 层应该保持纯粹:只做连接和治理,不做业务决策。业务规则该写在业务系统里,就写在业务系统里,别动不动搬进来。
我后来复盘整个项目,最大的体会是:Agent 的价值不在于它有多会聊天,而在于它能不能稳定、可控地碰到真实世界的系统。Agent-Reach 这个名字起得很大,但实现起来其实就是老老实实做好“翻译、路由、校验、回传”这四件事。如果你也在做 Agent 应用,希望这篇复盘能帮你少踩几个我踩过的坑。