我最早做AI Agent相关项目的时候,踩过一个特别典型的坑:模型选的是当时最强的,Prompt也反复调了好几个版本,Demo演示的时候各种丝滑,结果一接到真实业务场景,Agent就开始"满嘴跑火车"——让它查一下内部知识库里的流程文档,它说"根据我的知识,这个流程大概是……",然后一本正经地编了一段根本不存在的规定。
问题出在哪儿?不是模型不行,也不是Prompt不行,而是Agent的"手和脚"太短了。模型本身只有一堆训练参数,它连接不到你的数据库、读不到你上传的PDF、也按不了网页上的按钮。你给它再强的推理能力,它也只能在你塞给它的那点上下文里"盲猜"。这个问题的本质,就是Agent的触达能力(Reach)不够:它够不到实时数据、够不到业务系统、够不到互联网上那些还没进入训练集的长尾信息。
我想了很多办法去解决这个问题,最后沉淀下来一套思路,名字就叫"Agent-Reach"。核心就一个字:够。把Agent需要的一切外部能力,拆成显式的、可插拔的触达组件,让模型只负责规划和决策,真正去"够东西"的动作,全部交给一个我们完全可控的执行层。这篇文章是我自己做这个项目的一次完整复盘,包括架构设计、最小实现、踩过的坑,以及一些在文档里翻不到的实操经验。适合正在做Agent落地、或者准备把Agent从一个"聊天机器人"升级成"能干活的机器人"的工程师和产品同学。
1. 为什么多数Agent项目"看起来厉害,一接业务就废"
先说一个我观察到的现象:很多人做Agent,第一个版本永远是"给我写一首关于春天的诗"、"帮我整理一下这份文档的要点"这类任务。这类任务模型本身就能完成,跟Agent沾边但本质上只是一个"带上下文的高级Chat接口"。一旦进入真实业务,比如"根据最近一周的销售数据,找出去年同类产品推广中最有效的三个渠道,并生成一份下周投放建议",问题就来了——数据在哪儿?历史推广记录在哪儿?模型根本不知道。它只能开始编。不是说模型想骗你,而是它手里没有真实资料,不编就只能沉默,而沉默对Agent来说等于失败,所以它宁可编。
我把这类问题总结成三个"看不见的手铐"。
1.1 上下文囚笼:你的知识库塞不进对话窗口
现在的模型上下文窗口看着很大,128K、200K甚至更大,但对比真实业务数据,这点窗口其实小得可怜。我手头有个内部SOP文档,加起来一千多页,转成纯文本大概一百五十万token,就算全部塞进上下文窗口,先不说成本,模型处理这么长的内容时,注意力早就被稀释得差不多了,后面讲什么它根本记不住。
更关键的是,你不可能每次对话都把全量知识库塞进去。真正的解法是让Agent知道"我需要的时候就去找",也就是具备检索触达能力。Agent-Reach落地时,第一步就是给Agent装上一套"检索的手":用向量召回加关键词召回的方式,把知识库变成一堆可检索的碎片,Agent需要哪块就去捞哪块,而不是指望它把整个图书馆背下来。
1.2 手脚被绑:模型只能输出文字,干不了任何实事
模型的输出本质上就是一个字符串。它没法自己去查数据库、没法调用支付接口、没法登录你的内部CRM。早期智能体只能做到"我告诉你SQL,你自己去执行",这中间只要多一层人工拷贝,Agent的自动化价值就废了一半。
后来大模型厂商都支持了函数调用(Function Calling)或者工具调用(Tool Use),模型可以在回复里生成一个结构化的调用指令,比如"调用search_web工具,参数是:关键词=XXX"。但这里有个认知误区:模型只是"决定"要调用工具,真正执行工具的还是我们的代码。Agent-Reach在这一点上做了一个很关键的拆分——把"模型决策"和"代码执行"彻底解耦。模型永远不直接连接什么东西,它只输出工具名和参数,真正发起HTTP请求、打开浏览器、查询数据库的动作,全部由Agent-Reach的触达层去完成。这样既发挥了大模型的规划能力,又把不可控的部分锁在了一个可控的沙箱里。
1.3 记忆没长脚:每次对话都像第一次上班的新人
做过客服类Agent的朋友应该很有感触:用户上午问过一个工单进展,下午再问一次,Agent已经完全不记得了。所有信息都要靠外部系统重新捞一遍。这是"记忆触达"的缺失——Agent不是没有记忆,而是它不知道去哪儿找记忆。
印象很深的一次:我们给一个售前机器人加了历史会话记忆库,结果机器人确实会去查了,但查的是我们自己塞进Prompt里的一段JSON摘要,那段JSON一长,上下文就爆了。后来改成用向量检索按需拉取历史对话片段,效果立刻好了很多。这件事让我意识到,记忆不只是一个存储问题,更是一个触达问题:要让Agent具备"主动去够历史经验"的能力,而不是被动接受塞给它的东西。
2. Agent-Reach到底在"够"什么:四类触达目标与取舍
想清楚"够"这个概念之后,下一步是拆解:一个真实业务里的Agent,到底需要够哪些东西?我做了几个项目之后,把触达目标分成了四类,每一类的底层技术差别很大、代价也完全不同,事先分清楚能省掉大量返工。
2.1 触达本地知识库:从"全量灌输"到"按需检索"
这是最常见的需求——把公司内部的文档、FAQ、过往方案变成Agent可以随时调用的知识。最朴素的做法是把文档全部灌进向量数据库,然后做相似性检索。但单纯的向量检索有一个很典型的毛病:对"说法变了但意思一样"的问题召回好,对"包含精确代号、型号、编号"的问题则经常翻车。
比如你问"PRD-2024-0815号项目的验收标准是什么",如果文档里写的是"2024年8月15日启动的XX项目",纯向量检索很可能因为字面差异太大而召回不到。所以我的经验是,不要在向量检索一棵树上吊死,至少要做"向量召回 + 关键词召回"的双路混合,再合并排序。Agent-Reach在知识库触达这一块,实际上封装的就是这类混合检索逻辑,把底层用什么数据库、用什么Embedding模型、怎么合并分数这些事屏蔽掉,给Agent暴露一个简单的接口:问一句自然语言,返回N个最相关的片段。
2.2 触达外部服务:工具调用的实战形态没那么简单
函数调用现在大家都会写,比如给模型一个get_weather的JSON Schema,它就会返回{"city": "北京"}之类的参数。但真正到生产环境,工具触达要面对的问题远不止"让模型返回一段JSON"。
我遇到过的最大坑是工具返回值的长度和格式不可控。有的API返回一坨几万字的JSON,直接塞进上下文,模型看着看着就"疯"了,开始胡言乱语。Agent-Reach的做法是给每个工具的输出都做一次"压缩适配":要么在工具侧先用代码提炼出关键字段,要么在把结果喂给模型之前做一次摘要。原则很简单:保证模型的上下文里只出现"决策所需的信息",而不是"触达到的原始数据"。这个原则我后面会反复强调,几乎所有Agent翻车都跟违反它有关。
2.3 触达真实界面:当API不存在时,浏览器就是你的接口
有些老系统,没有开放API,甚至连数据库都不让直连,你想让Agent"帮用户查一下订单状态",能怎么办?唯一的通路是浏览器。Agent-Reach里我保留了这样一个模块:基于Playwright的浏览器触达器,让Agent可以打开页面、填表单、点按钮、读结果。
但是这里有个容易搞混的点:浏览器触达和普通爬虫是两回事。爬虫是写死路径去抓固定字段,浏览器触达则要面对"页面结构变了怎么办""弹窗挡住了怎么办""登录态过期了怎么办"。我的方案是给浏览器触达器加了一层视觉级兜底——当DOM选择器定位失败时,降级为截图并用视觉模型识别页面元素的位置,再通过坐标点击。这个方案不算完美,但确实把Agent在真实网页上的容错能力提高了一大截。
2.4 触达记忆与协作:让Agent拥有"过去时"和"另一个人"
前面提到过,记忆不是存储问题,是触达问题。Agent-Reach里把记忆分成了两层:
- 短期工作记忆:当前会话里已经聊过的内容,通过摘要不断压缩,防止爆上下文。
- 长期项目记忆:已经沉淀的事实、结论、用户偏好,存在向量库里,Agent在做决策前按需检索。
这两层之外,还有一类容易被忽略——跨Agent的共享经验。我们当时有两个Agent在跑同一个业务流程,一个负责售前咨询,一个负责售后跟进,用户先咨询再报修,售前Agent聊的内容,售后Agent完全不知道,用户就要重复一遍。后来我们把两个Agent接到同一个记忆库,售后Agent在开场时会主动检索"这个用户最近是否咨询过其他问题",体验一下就顺了。所以记忆触达还要解决"谁的经验可以被谁够得到"的问题,这本质上是一个权限和共享的设计。
3. 最小可用的触达层长什么样:一个会"查资料+写摘要"的骨架
理论讲完,上点能用的东西。我搭Agent-Reach早期版本的时候,没有一上来就上那种重型的Agent框架,所有编排逻辑先用一个两百行的Python骨架跑通。这里把核心设计分享出来,你可以直接照这个思路去搭你自己的触达层。
3.1 整体结构:决策与执行严格分离
我的代码结构只有三层:
- 编排层(Orchestrator):接收用户问题,交给大模型,由大模型决定要调用哪个工具,传入什么参数。
- 触达层(Reach Layer):真正执行工具调用的地方。每一个工具都是一个独立函数,有输入校验、超时控制、返回结果统一包装。
- 合成层(Synthesis):把多个工具返回的结果交给大模型,由大模型整合成最终回复。
核心思想是,编排层永远不知道工具是怎么实现的。它只看到一张工具清单:工具叫什么、是干什么的、需要什么参数、参数长什么样。至于这个工具是去调GPT的接口还是去操作浏览器,编排层一概不管。这样做的好处是便于测试:工具可以单独单测;便于审计:每个工具调用都留下了入参和出参的日志;更便于替换:今天用某家的搜索API,明天换另一家,对Agent来说完全无感。
3.2 触达层核心代码:注册表加统一执行器
下面这段代码是这个骨架里最关键的部分,一个极简工具注册表加统一执行器。你不需要原样抄,理解这个设计思路就够了。
# agent_reach_reach_layer.py # Agent-Reach 触达层最小骨架 from typing import Any, Callable, Dict, Optional from dataclasses import dataclass, field import time import json import logging logger = logging.getLogger("agent_reach") # 工具返回值的统一包装:不管是API返回、数据库查询还是抓取的网页, # 一律包装成这个结构,避免后续处理时格式混乱。 @dataclass class ReachResult: tool_name: str status: str # "success" / "failed" data: str # 已经压缩提炼后的文本,直接可喂给模型 raw_data: Any = None # 原始返回,主要用于日志审计 duration_ms: float = 0.0 def _default_validator(params: Dict[str, Any]) -> Dict[str, Any]: """默认参数校验,实际项目中建议用 pydantic / jsonschema。""" return params @dataclass class ReachTool: name: str description: str parameters_schema: dict execute: Callable[[Dict[str, Any]], str] = None timeout_seconds: float = 10.0 validator: Callable[[Dict[str, Any]], Dict[str, Any]] = _default_validator # 工具注册表:所有可被Agent调用的触达能力都注册到这里 class ToolRegistry: def __init__(self): self._tools: Dict[str, ReachTool] = {} def register(self, tool: ReachTool) -> None: if tool.name in self._tools: raise ValueError(f"工具重名: {tool.name}") self._tools[tool.name] = tool def list_tools_for_llm(self) -> list[dict]: """把工具清单转换为模型Function Calling需要的格式""" specs = [] for tool in self._tools.values(): specs.append({ "type": "function", "function": { "name": tool.name, "description": tool.description, "parameters": tool.parameters_schema, } }) return specs def has_tool(self, name: str) -> bool: return name in self._tools def get_tool(self, name: str) -> Optional[ReachTool]: return self._tools.get(name) # 统一执行器:负责入参校验、超时保护、日志记录、返回值包装 def run_tool(registry: ToolRegistry, tool_name: str, params: Dict[str, Any]) -> ReachResult: # 防御:模型传了不存在的工具名 if not registry.has_tool(tool_name): return ReachResult(tool_name=tool_name, status="failed", data=f"工具未找到: {tool_name}") tool = registry.get_tool(tool_name) start = time.monotonic() try: # 入参校验很关键,模型偶尔会生成不符合Schema的参数 validated = tool.validator(params) if tool.execute is None: return ReachResult(tool_name=tool_name, status="failed", data="工具无实现") # 统一在这里做超时保护,防止第三方API挂死 # 简化写法,生产环境建议用 asyncio.wait_for 或线程池 raw_output = tool.execute(validated) duration_ms = (time.monotonic() - start) * 1000 # 关键一步:把工具输出截断到合理长度,防止污染模型上下文 # 这个阈值可以根据你的模型窗口调整,我用的是2500字符 max_chars = 2500 output_text = str(raw_output) if len(output_text) > max_chars: output_text = output_text[:max_chars] + "\n...[截断]" logger.warning("工具 [%s] 返回超过 %d 字符,已截断", tool_name, max_chars) return ReachResult(tool_name=tool_name, status="success", data=output_text, raw_data=raw_output, duration_ms=round(duration_ms, 2)) except Exception as exc: # 异常绝不落进模型上下文,而是转成固定格式的错误信息 duration_ms = (time.monotonic() - start) * 1000 logger.exception("工具 [%s] 执行异常: %s", tool_name, exc) return ReachResult(tool_name=tool_name, status="failed", data=f"工具执行失败: {str(exc)[:200]}", duration_ms=round(duration_ms, 2))这套骨架用起来很直接:写工具函数、注册到registry、将registry里的工具清单传给模型的Function Calling配置。模型返回要调用哪个工具、传什么参数之后,直接交给run_tool执行,再把ReachResult.data塞回对话历史,让模型继续总结。就这一个闭环,已经足够支撑非常多真实场景。
3.3 为什么这样设计:三个反直觉的决策
第一,工具返回值强制截断。很多人的直觉是,工具返回越详细越好,模型看得越多答得越准。但我实测下来恰恰相反,一旦某次触达返回超过一定长度,模型对后续指令的遵循能力会明显下降。信息密度比信息总量重要得多。我习惯在工具函数内部就做好提炼:比如查订单,在函数里直接执行SQL,然后返回的不是原始表而是"该订单状态为已发货,物流单号SF123456,预计明天到达"。这样模型拿到的就是可以直接用的信息。
第二,异常不交给模型处理。刚开始做的时候,有一次搜索API超时,返回的是一大段堆栈错误,模型看到之后一本正经地分析"可能是网络不稳定、也可能是防火墙设置有问题",非常危险,因为它可能就此开始"猜测"而不是去重试或者换路。现在我把所有异常统一转成"工具执行失败:超时",模型就只会做两件事:换个工具再试,或者告诉用户现在查不了。可控多了。
第三,工具清单不要一口气全开。注册表里放二十个工具有时候反而是灾难。模型面对的工具选择越多,选错的概率就越大,Prompt里工具描述还会占用大量token。我的经验是,按场景动态开放工具:售前机器人只开放订单查询、产品库检索、知识库搜索这几个工具;售后机器人则开放工单创建、进度查询等。Agent能够到的范围,先窄后宽,跑稳了再逐步加。
4. 实测三次翻车:上下文阻塞、来源幻觉、并行错序的完整排查链路
骨架搭好只是开始,真正的教室是生产环境。我在这套系统上跑了大概两个月,中间翻过三次比较大的车,每次都是查日志查到大半夜才定位到根因。这几次教训我觉得比架构本身更值得拿出来说。
4.1 第一次翻车:一次搜索几乎吞掉全部上下文窗口
现象是这样的:Agent在回答一个关于竞品分析的问题时,突然开始答非所问,甚至把用户之前问过的问题又重复了一遍。起初怀疑是模型抽风,后来看token日志才发现,那次会话中某个搜索工具返回了大约十篇网页的全文摘要,加起来接近六万个token,直接把上下文窗口占满了。后面模型不是在思考,而是在"遗忘",前面聊的内容、工具返回的内容全被挤出窗口了。
排查链路:先看每次触达返回的字符数,再统计历史对话中模型开始"失忆"的时间点,发现无一例外都发生在某次超长工具返回之后。根因确认后,我在两个环节做了修复:第一,工具侧按域名和正文结构做抽取,只返回标题、发布时间、正文前几百字;第二,在run_tool里加绝对上限,超过阈值直接截断并打日志。这个修复上线后,"失忆"问题基本绝迹。
4.2 第二次翻车:引用来源张冠李戴,Agent开始"幻觉"来源
第二次的问题更隐蔽。我们的Agent会在回答末尾附上引用来源,比如"[1]、[2]、[3]",这是通过Prompt要求模型做的。某次用户问"对比一下A产品和B产品在售后政策上的区别",Agent答得头头是道,结果我点开引用链接一看,[1]链接指向的是A产品的页面,但正文里那句关于[1]的描述,实际上是B产品页面的内容。
排查链路:去翻日志,发现那次回答之前,触达层并行检索了六个网页,返回给模型的时候,只是简单地拼接成一个列表。模型在处理这些并列来源时,把编号和内容搞混了。根本原因是:多个来源并列返回时,没有在结构上强制建立"编号=内容"的强绑定。文本拼接的格式,模型可以读对,但压力大时就会错。
修复方式:把并列返回改成结构化块,每块先出现[来源ID],然后是URL,然后是内容;同时Prompt里明确要求"引用时只能基于[来源ID]对应的内容,不得混用"。说到底,不是模型不够聪明,而是我给它制造了一个容易混淆的输入结构。从那以后,我给触达层的所有多返回值都定了规矩:先标识来源,再给内容;来源和内容永远是一体的,拆开就是事故。
4.3 第三次翻车:并行调用的返回顺序乱了,输出驴唇不对马嘴
触达层支持并发之后出过一个大乱子:Agent同时调用了"查天气"和"查航班"两个工具,两个结果几乎同时返回,由于实现里把结果按返回顺序填进了消息历史,结果模型读到的是"天气:国航CA1234延误","航班:晴,28度"。它自己都困惑了,最后给用户输出了一段颠三倒四的回复。
排查链路:在日志里比对工具调用ID和结果ID,发现我的代码压根没有给并发任务打标签。不同触达的返回只是简单地append,顺序完全取决于哪个先回来。找到根因后,我立即改为"按调用ID分组再拼接",同时要求编排层在模型执行前,先拿到完整的工具清单和工具返回值,按固定顺序塞回上下文,而不是逐个append。这个坑让我体会到,在工程上,模型对输入顺序是非常敏感的,任何不确定的顺序,最后都可能变成输出里的混乱。
4.4 三次翻车的共性:问题都不在模型
仔细复盘这三次事故,我发现一个共性:每次翻车,根因都不在模型,而在触达层的数据流设计。模型只是忠实地消费了我喂给它的东西,我喂的是混乱、超长、错序的信息,它就只能产出混乱的结果。这就是为什么我一直强调"Agent质量的上限取决于触达层的整洁度"——模型推理能力再强,也顶不住脏乱差的数据流。所以后来每写一个工具,我都会先问一遍:这个工具返回的东西,一个刚入职的实习生拿到手里,能不能不迷糊?如果实习生会迷糊,模型也会迷糊。
5. 从"能跑"到"稳定跑":缓存、退避、校验与安全栏
Agent-Reach跑通功能之后,我花了大把时间在生产化上。这个阶段的收益,比调Prompt高得多。抽象地说,就是把触达层从"能被调用"打磨成"可以被信任"。
5.1 结果缓存:同样的触达,不要花第二次钱
Agent在真实使用中,有很多查询是高度重复的。比如"退款政策是什么"这种高频问题,触达层每次都要去检索知识库、甚至调用外部搜索,既慢又烧钱。我的方案是加一层结果缓存:以"工具名 + 参数哈希"作为key,把触达结果缓存一段时间。实测下来,在我们的客服场景里,缓存命中率能做到四成以上,响应时间从四五秒降到一秒以内,API费用也省了将近三分之一。
缓存需要考虑失效策略。我当时用的是"TTL + 主动失效"的组合:知识库类触达缓存一小时,订单类触达缓存五分钟,涉及库存、价格的触达直接不缓存或者缓存三十秒。数据更新时,由写入方主动调用失效接口,把相关key打掉。这一套对于大多数业务场景够用了。
5.2 限流、超时与指数退避:第三方API不是你家服务器
触达层一旦跑起来,第三方API的限流就成了家常便饭。我见过最离谱的一次,某个外部接口在高峰期连续返回429,我的Agent在短时间内重试了十几次,把对方彻底惹毛,直接封了我们的IP。
修复思路有两条腿。第一条是"别让Agent把重试当饭吃":触达层统一接管重试策略,对429和5xx做指数退避,第一次等1秒、第二次2秒、第三次4秒,最多重试三次,重试还不行就降级。第二条是"限流前置":在触达层做一个简单的令牌桶,每秒最多调用N次该工具,超出的请求直接排队等待,而不是一窝蜂打到第三方。做完这两件事,我们的工具调用故障率降了一个数量级。
5.3 返回结果校验:脏数据进不了上下文,幻觉少一半
工具返回的数据五花八门,有些字段缺失、有些格式错乱、有些干脆是HTML标签串。把这些脏数据直接喂给模型,模型会说"根据以上信息"然后开始一本正经地胡说。Agent-Reach在触达层加了一道"结果校验闸门",用JSON Schema做校验:字段缺了补默认值,类型错了做转换,完全不合法的直接标记为失败,让Agent换一条路。这一步要花点时间把每个工具的返回格式定义清楚,尤其是那些对接已久的老接口,但收益非常直接——模型吃到的数据干净,输出自然干净。
5.4 安全边界:让Agent决定"做什么",但别让它独立"执行危险动作"
最后一条,也是最重要的一条:触达层必须对"危险动作"有硬性的安全护栏。什么是危险动作?删除记录、修改配置、发送对外消息、支付转账,这些都属于不可逆或高影响操作。
我的做法是给工具打标:普通工具Agent可以自主调用;危险工具走"人工审批"流程。Agent照样会生成调用指令,但触达层拦截下来,生成一条审批请求推到钉钉或企业微信,人点同意才真正执行。同时,触达层的完整调用日志做了审计,谁在什么时候调了什么工具、传了什么参数、得到了什么结果,全部可追溯。这个东西一定要在生产化初期就建好,后面再补,成本会翻好几倍。
6. 不要神化Agent-Reach:哪些场景根本不该上这套东西
每篇文章都在告诉你怎么做,我来泼一点冷水:并不是所有项目都适合上Agent-Reach这套触达架构。如果符合下面几种情况,我建议你先用普通程序解决,别硬凹Agent。
6.1 流程固定、规则明确时,定时任务或脚本远好于Agent
如果业务需求是"每天凌晨把昨天订单汇总成表格",这个流程是固定的、可预期的,直接写脚本、上定时任务,三百行代码就搞定,稳定又省钱。如果用Agent来做,等于让一个"灵感型选手"去干"流水线工人的活",模型每次决策还会有一点点随机性,反而引入了不确定性。Agent-Reach的价值场景是"路径不可预先穷举、需要动态决策"的任务。如果一个任务你闭上眼睛都能写出执行步骤,那它根本不需要Agent。
6.2 输入输出完全结构化时,传统接口效率碾压
还有一种常见的误区:以为带上了AI就有智能化。比如某个系统原来用Webhook接收JSON,处理完再回传JSON,中间逻辑非常明确。有人问能不能改成Agent,我说改了之后唯一的变化就是延迟从200毫秒变成3秒、成本从几分钱变成几块钱,准确率还未必有原来的高。Agent擅长的其实是非结构化输入、模糊意图、开放式输出。两边搞反了,就是把好东西用错了地方。
6.3 高一致性、强事务场景,谨慎引入
涉及资金、法务、医疗这些要求强一致性的场景,Agent的"概率性正确"就有风险。不是说不能用,而是要在Agent外面套上强规则壳:Agent负责生成建议,规则引擎负责校验建议是否合规,双人复核甚至多人复核之后才能执行。我帮一个金融客户做过类似的落地,最后的方案是Agent只负责做信息整理和风险提示,具体交易动作全部由传统流程引擎控制,Agent没有最终决策权。这个边界划清楚之后,项目的推进顺畅了很多。
6.4 我的取舍建议:别让"触达能力"反过来绑架你的系统
如果你看完这篇还是觉得Agent-Reach这套思路可以试,我给一个路线图:先挑选一个最痛、路径最杂、通常是人工翻阅十几个系统才能完成的业务场景,做一次最小闭环。工具数量控制在五个以内,先把触达层的规范立起来:返回值结构、超时控制、异常协议、缓存策略。跑通之后再横向复制到其他场景。我在实践中反复体会到一个道理:Agent项目的复杂度几乎全部来自触达层,模型本身反而简单。触达层做得越规整,Agent的表现就越稳定。
最后分享一个细节:我后来把所有工具注册表里的工具描述集中review了一遍,把那些形容词太多、暗示性太强的描述全部改成"简短客观"的写法,比如把"这个工具很好用,可以快速查询订单信息并返回详情"改成"查询订单信息,按订单号返回订单状态、金额、物流"。模型的工具选择准确率立刻就有肉眼可见的提升。模型不需要你夸工具,它只需要知道工具能干什么、什么时候该用。这大概也是Agent-Reach这条路的缩影:与其去调教模型,不如去打磨它可以够到的世界。