ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Agent-Reach:大模型Agent工具调用与执行中间层实践

Agent-Reach:大模型Agent工具调用与执行中间层实践 做 Agent 的人大概都有过这种体验模型在对话框里聊得头头是道一旦让它真的去查一次库存、读一个仓库里的配置文件、调一个内部接口它就开始原地打转——要么反复问你要参数要么编一个看起来很像那么回事的假结果给你。问题往往不在模型不够聪明而在于它够不着。Agent-Reach 这个项目名把矛头指得很准Reach触达。它要解决的核心矛盾不是模型推理得对不对而是模型想做的事能不能真的落到外部世界里去。从命名和同类 Agent 项目的普遍做法来看Agent-Reach 定位的是一层执行中间层把模型的意图翻译成具体动作把动作的执行结果再翻译回模型能读懂的上下文。模型负责想Reach 负责让这个想有地方落地。这个中间层里塞着工具注册、路由识别、上下文预算、执行循环、失败重试、记忆存取、多 Agent 之间的消息传递以及一整套安全与可观测性的兜底机制。它不炫技但它决定了你的 Agent 是玩具还是能干活的东西。这篇文章适合三类人看第一类是想从零搭 Agent 但被一堆名词绕晕的新手第二类是已经能跑通 demo、但一上真实业务就各种超时和幻觉的开发者第三类是正在做架构选型、纠结要不要上多 Agent 的技术负责人。我不会给你一份万能模板而是把每一层为什么这么设计、参数怎么算、坑在哪里尽量说透。1. Agent-Reach 到底想解决什么问题1.1 从会说到会做中间缺的那一段纯 LLM 的输出是文本它的世界观只有 token。你问它上海今天天气怎么样它只能基于训练数据里的统计规律给你一个听起来合理的答案它并没有真的去查。这就是所谓的知识截止和幻觉问题的共同根源——它没有通道去接触实时世界。Agent 这个概念的价值就在于给 LLM 装上手脚和记忆。手脚是工具调用记忆是上下文管理和外部存储。而 Agent-Reach 这类项目要做的是把装手脚这件事工程化、标准化。因为裸着写工具调用你会发现几个立刻冒出来的问题工具有几十个的时候全部塞进 system prompt 会撑爆上下文模型选错工具的几率随着工具数量上升而飙升工具执行失败之后模型不知道该重试还是该换个思路并发执行多个工具时结果回来的顺序和依赖关系会乱。这些问题单靠提示词写得好一点是解决不了的必须有工程手段。举个最常见的例子你给 Agent 挂了 40 个工具每个工具的 schema 加描述平均 80 个 token光工具定义就吃掉 3200 token。这还没算对话历史。模型在这么多工具描述里做选择准确率会明显下滑。Reach 层的第一件事就是把这个选择过程收窄——先判断意图属于哪个大类再把该类的 3 到 5 个工具喂给模型去精挑。这就是路由识别节点存在的意义。1.2 一次名词清理Agent、Skill、Harness、LLM、Embedding 到底谁是谁这块是新手最容易糊的地方我用一个类比一次性讲清楚。把 Agent 想象成一家餐厅LLM 是厨师长负责决策和创造Embedding 是仓库管理员的索引卡片负责把番茄炒蛋和你仓库里的食材对应起来Skill 是菜谱告诉执行者这道菜分几步做工具是灶台和锅是真正干活的东西Harness 是整个厨房的排班、传菜、清洁和监控系统。具体到定义上LLM 是语言模型本体输入文本输出文本没有状态没有记忆不会主动做事。Embedding 是把文本映射成高维向量的模型主要用于相似度检索和召回它不生成内容只负责找得像。Skill 通常指一项被封装的、可复用的能力单元它可能是若干工具的编排也可能带自己的提示词模板和输入输出约束。工具是原子能力比如发一封邮件查一次数据库。Agent 是 LLM 加上循环、加上工具、加上状态的一个运行时实体。Harness 是承载 Agent 运行的工程外壳负责调度、上下文拼接、工具分发、错误恢复和轨迹记录。还有一个极易混淆的点Java Agent 和 AI Agent 完全不是一回事。Java Agent 是 JVM 层面的一种字节码增强机制通过 premain 或 agentmain 挂到进程上用来做监控埋点、性能分析、类替换。它和 LLM 没有任何关系只是名字撞了。如果你在搜索 Agent 开发资料时刷到一堆 Instrumentation、ClassFileTransformer 的内容别怀疑人生你只是搜错词了。1.3 我做方案选型时的三条判断标准在决定自研 Reach 层还是直接用现成框架时我一般看三件事。第一工具数量和变更频率。低于 10 个工具且半年不变直接用框架内置的工具注册就够了没必要自研路由。超过 30 个工具或者工具每周都在增删就必须有独立的工具注册表和路由层否则每次加工具都要改核心提示词维护成本会失控。第二执行时长和失败代价。如果每个工具调用都在 1 秒内返回同步执行完全没问题。如果有工具要跑几分钟比如跑一次数据同步就必须把执行做成异步任务加轮询或回调还要设计幂等键否则网络抖动一次就会产生重复写入。这是我在真实项目里踩过最疼的坑之一。第三是否需要跨团队协作。如果只是自己一个团队用单体架构最省事。如果多个团队各自维护自己的 Agent还想互相调用那 A2A 这类协议和 agent card 就得提前规划后补的成本极高。2. 整体架构把 Reach 拆成五层2.1 感知层与路由识别节点的定位感知层干的事是把用户输入和历史上下文整理成模型能处理的干净输入。听起来简单实际充满细节要不要做意图归一化要不要做敏感内容过滤多轮对话里指代怎么消解它那个上面说的方案这些词如果直接丢给模型模型很容易脑补出错误的指代对象。常见的做法是在感知层加一个轻量的指代消解步骤把最近几轮的关键实体做一次显式绑定。路由识别节点是感知层之后、规划层之前的一个分类环节。它的目标不是选具体工具而是判断这次请求该走哪条链路。比如用户问帮我看看昨天的订单为什么跌了路由应该判定这属于数据分析链路然后激活 SQL 查询类、指标计算类工具同时屏蔽掉内容生成类和通知类工具。这样做的好处非常实在工具子集变小模型选择准确率上升提示词长度下降响应速度也变快。关于路由的实现方式我的经验是先用规则和小模型不要一上来就再用一个大模型去分类。规则能覆盖 60% 到 70% 的高频场景成本几乎为零。剩下的模糊地带再用一个小的分类模型或 embedding 相似度匹配把用户 query 和预设的意图样例做最近邻。实测下来这种组合方案比全程用大模型做路由延迟低一个数量级准确率差距在可接受范围内。注意路由节点一定要有兜底分支。当所有意图的匹配分数都低于阈值时不要把请求硬塞给某个最相似的类而应该走通用链路或者直接向用户澄清。硬塞的结果通常是工具选错、参数乱填然后模型编一个看起来很合理的失败解释。2.2 规划层为什么我不建议一上来就上多 Agent规划层负责把做什么拆成分几步做。这里有个很诱人的陷阱一看到复杂任务就想上多 Agent 协作让一个负责规划、一个负责执行、一个负责审查。理论上很美实际上你马上会遇到三个问题。第一个是上下文传递损耗。Agent 之间通信靠消息消息本身要序列化再反序列化原始任务里的细粒度约束在多次转述后极容易丢失。我见过最典型的情况是用户要求只查 A 类商品传到第三个 Agent 的时候变成了查所有商品后面全错。第二个是成本和延迟的乘法效应。三个 Agent 各自带自己的 system prompt 和工具集每次协作至少 3 到 6 次模型调用token 消耗是三到六倍。如果每个 Agent 还要带长上下文成本会更夸张。第三个是调试难度。单体 Agent 出错你看一条完整轨迹就能定位。多 Agent 出错你要在多个 Agent 的轨迹之间做时间对齐搞清楚是谁先把信息传歪了。在没有完善的轨迹记录之前多 Agent 的调试体验非常糟糕。我的建议是先把单体 Agent 的规划能力做扎实用计划-执行-反思这种单体内循环把任务拆解和工具调用做顺。只有当任务确实可以并行、且子任务之间耦合很低时才考虑拆成多 Agent。判断标准很简单——如果两个子任务之间需要频繁交换中间状态那就不该拆。2.3 执行层与工具注册表执行层是 Reach 最实的部分。它要处理的是工具怎么注册、参数怎么校验、调用怎么超时、结果怎么截断、异常怎么分类。工具注册表建议用声明式定义每个工具至少包含这些字段名称、功能描述、参数 schema、参数示例、返回值结构、超时时间、是否幂等、权限等级、成本等级。后面四个字段很多人会省但它们在稳定性上作用巨大。是否幂等决定了失败后能不能无脑重试权限等级决定了这个工具能不能在无人确认的情况下自动执行成本等级决定了要不要对它做频率限制。参数校验必须在调用前做不要指望模型每次都填对。校验不通过时不要直接抛异常给模型看堆栈而应该返回一句人类可读的、指明了哪个字段什么问题的话比如start_date 格式应为 YYYY-MM-DD收到的是 2024/01/01。这样模型下一轮自我修正的成功率会高很多。我做过对比加了参数格式的友好提示之后一次成功率大概能从六成提到八成五左右。结果截断也是必修课。一个数据库查询返回几千行直接塞进上下文轻则爆窗口重则让模型忽略掉前面的关键指令。常见做法是限制返回条数同时对长文本做摘要或分页把完整结果在某某变量里这里是前 20 条这种元信息告诉模型。2.4 记忆层短期、情景、长期三档怎么分Agent 的记忆不该是一坨。我一般分三档来设计。短期记忆就是当前对话的上下文窗口特点是易失、容量有限、访问最快。它承载的是这次任务的所有临时状态。工程上要做的是滚动摘要——当对话超过阈值时把最早的若干轮压成一段摘要保留关键实体和已达成的结论丢掉寒暄和重复确认。情景记忆是上次遇到类似情况是怎么处理的。比如用户上次问退款政策我们最后引导到了哪个页面。这类记忆通常按任务类型归档用向量检索召回能显著减少重复澄清。落地方式是把每次任务的输入摘要 关键决策 最终结果存一条记录建 embedding 索引新任务来了先检索相似情景。长期记忆是稳定的事实性信息比如这个用户偏好用表格形式返回这个项目所有金额单位是人民币。它的特点是变更频率低、需要高置信度、写入要谨慎。我的做法是长期记忆必须经过显式确认才写入绝不自动固化否则模型一次误解就会被永久记住后面越滚越歪。记忆类型存储位置生命周期典型内容写入策略短期上下文窗口单次会话本轮对话、临时变量自动滚动情景向量库数周至数月相似任务的处理记录任务结束后自动归档长期结构化存储长期用户偏好、硬性约束需显式确认3. 动手实操搭一条最小可用的 Reach 链路3.1 环境准备与依赖先说清楚这是基于常见实践的参考配置不是某个项目的官方文档。Python 环境建议 3.10 以上因为要大量用类型注解和 match 语法。核心依赖不需要很多别被各种花里胡哨的框架绑架。python -m venv venv source venv/bin/activate pip install pydantic httpx tenacity pip install numpy pip install chromadbpydantic 用来做参数 schema 校验它能把 JSON Schema 生成和运行时校验一次搞定省掉手写校验逻辑。httpx 用于异步 HTTP 调用比 requests 更适合并发场景。tenacity 用来做重试策略指数退避、抖动、最大次数这些配置它都有现成的。chromadb 只是打个样生产环境你大概率会换成团队已有的向量检索服务。环境变量里至少要有模型 API 的地址和密钥以及一个总超时配置。我强烈建议把超时做成全局可覆盖的但每个工具可以声明自己的超时。默认 30 秒对大多数接口够用但涉及数据同步的工具可能要给到 300 秒以上。3.2 工具描述怎么写模型才认得这一节是我认为整个 Reach 里最被低估的部分。工具描述写得好不好直接决定模型选得对不对。我总结了几条实操规则。第一描述里必须包含什么时候用而不只是这个工具是什么。比如查询订单状态这种描述太弱更好的写法是当用户询问某个订单当前处于什么状态、是否已发货、预计何时到达时使用本工具。不适用于查询历史订单列表。后半句的负向说明能挡掉大量误用。第二参数描述要给例子。模型对格式的敏感度远低于对人类示例的敏感度。写了example: 2024-03-01比写日期格式为 ISO 8601有效得多。第三工具名用动词加名词不要用缩写。query_order_status比qos好一百倍别为了省那几个 token 牺牲准确率。第四给每个工具标一个使用前置条件。比如调用本工具前必须先获得有效的用户 ID这能减少模型在信息不全时硬编参数的概率。from pydantic import BaseModel, Field from typing import Optional class QueryOrderStatusArgs(BaseModel): order_id: str Field( ..., description订单编号通常为 18 位数字。示例202403011234567890 ) include_logistics: bool Field( False, description是否同时返回物流轨迹。开启会增加约 1 秒耗时仅在用户明确询问物流时开启 ) TOOL_SPEC { name: query_order_status, description: ( 查询单个订单的当前状态返回订单状态、支付时间、发货状态。 适用场景用户询问某个具体订单现在怎么样了。 不适用查询订单列表、按条件批量筛选订单。 ), args_model: QueryOrderStatusArgs, timeout: 30, idempotent: True, permission: read, }3.3 上下文预算的算法与长上下文取舍很多人听说过长上下文范式就觉得窗口越大越好其实不然。窗口变大之后模型对中间位置信息的注意力会衰减这就是常说的中间遗忘。而且长上下文的推理成本是线性甚至超线性增长的延迟和费用都会上去。我给一个我自己在用的预算算法思路是把窗口分成几块固定的配额先保证刚性需求再分配弹性需求。def plan_budget(total_tokens: int) - dict: # 预留输出空间太小的输出配额会导致回答被截断 reserve_output min(8192, int(total_tokens * 0.08)) usable total_tokens - reserve_output # 系统提示词与角色设定固定配额 system_budget min(2000, int(usable * 0.05)) # 工具描述按当前路由选中的工具数量动态计算 # 每个工具大约 80 到 150 token tool_budget_cap int(usable * 0.15) # 记忆召回情景记忆注入上限 memory_budget int(usable * 0.15) # 剩余的给对话历史和工具返回结果 history_budget usable - system_budget - tool_budget_cap - memory_budget return { output: reserve_output, system: system_budget, tools: tool_budget_cap, memory: memory_budget, history_and_results: history_budget, }按这个算法128k 窗口下系统提示词 2000 token 左右工具描述最多占 1.5 万 token记忆召回 1.5 万 token历史加结果剩下大概 9 万多。实际用的时候工具描述和记忆两块几乎永远用不满剩下的配额会自动让给历史这是有意设计的弹性。真正要注意的是工具返回结果的截断策略。我的做法是给每个工具声明一个max_result_tokens超过就截断并附带一句结果已被截断完整数据共 N 条如需更多请分页查询。同时优先保留结构化的头部信息因为模型通常先看结构再决定要不要细节。提示不要把工具返回的原始 JSON 直接塞进上下文。先做一次字段裁剪只保留模型需要的字段。我见过一个接口返回 40 个字段实际有用的只有 6 个白白吃掉大量预算。3.4 执行循环、超时与失败重试执行循环的骨架其实不复杂难的是异常分支。下面是我常用的简化版本重点看重试条件和错误分类。import asyncio from tenacity import retry, stop_after_attempt, wait_exponential_jitter class ToolError(Exception): 可重试错误网络抖动、下游限流 class FatalToolError(Exception): 不可重试参数错误、权限不足 retry( stopstop_after_attempt(3), waitwait_exponential_jitter(initial1, max10), retrylambda e: isinstance(e, ToolError), ) async def call_tool(tool, args, idempotent: bool): if not idempotent: # 非幂等工具不做自动重试避免重复副作用 return await tool.execute(args) return await tool.execute(args) async def run_loop(plan, tool_registry, runner): for step in plan.steps: tool tool_registry.get(step.tool_name) try: async with asyncio.timeout(tool.timeout): result await call_tool(tool, step.args, tool.idempotent) step.result trim(result, tool.max_result_tokens) except asyncio.TimeoutError: step.result 超时该工具在 {} 秒内未返回请考虑简化查询条件或稍后重试.format(tool.timeout) except FatalToolError as e: step.result 调用失败且不可重试{}请检查参数或权限.format(str(e)) except ToolError as e: step.result 多次重试后仍失败{}建议改用其他方式获取该信息.format(str(e)) return plan这里有两个细节值得强调。第一asyncio.timeout包住的是整个重试过程还是单次调用这两种设计的语义完全不同。我上面是包住整个重试过程意味着三次重试的总时长不能超过工具超时。如果你希望每次重试都有完整超时那就要把 timeout 放到 call_tool 内部。选哪种取决于你的业务对总延迟的容忍度。第二错误信息要写成模型能理解的自然语言而不是异常类型名。模型看到TimeoutError只能猜看到请考虑简化查询条件就知道下一步该干什么。这一个小改动对 Agent 自我修复能力的提升非常明显。4. 多 Agent 协作与 A2A 协议落地4.1 agent card 是什么为什么它是协作的入场券当你要让两个不同团队开发的 Agent 互相调用时第一个问题就是我怎么知道对方能干什么agent card 就是回答这个问题的结构化描述。它通常包含 Agent 的标识、名称、描述、能力列表、支持的输入输出格式、鉴权方式、以及可访问的端点地址。你可以把它理解成一张名片加一份服务清单。没有它A 团队的 Agent 想调用 B 团队的 Agent就得靠人去读文档、硬编码接口一旦 B 方升级了能力A 方就挂。有了 agent card能力发现可以自动化——去对方的 well-known 路径拉一张卡片解析出能力清单动态注册成自己的可调用工具。关于版本的差异普遍情况是不同版本在卡片字段的表达上有调整比如能力描述从扁平列表演进为带参数约束的结构化对象鉴权声明从自由文本变成明确的类型枚举。做兼容时我的建议是把卡片解析做成容错模式必填字段缺失时给出明确告警而不是直接崩同时缓存卡片并设置合理的过期时间避免每次调用都去拉一次。落地要点有三个卡片的 available 端点要独立于业务端点方便探活能力描述里的示例要真实可跑不要写理想化的伪示例版本号一定要显式声明并在请求头里带上否则你永远不知道自己是在和哪个版本对话。4.2 三种协作拓扑的适用场景对比拓扑结构适合场景主要风险主从式一个协调者分发任务给执行者任务可清晰分解、执行者能力单一协调者成为瓶颈与单点对等式多个 Agent 平等协商需要多视角论证、评审类任务通信次数爆炸、收敛慢流水线式前一个的输出是后一个的输入步骤固定、顺序明确上游错误被逐级放大主从式最容易落地也最推荐作为起点。协调者只做分发和汇总不做具体执行执行者各自只懂自己的领域。对等式适合那种让三个角色分别写方案再互相批的评审场景但要严格限制轮次我一般不超过 3 轮否则会陷入无休止的互相补充。流水线式的关键在于每一步都要有校验不能等到最后一步才发现数据对不上。4.3 消息一致性与串味问题多 Agent 协作里最隐蔽的问题是上下文串味。具体表现是Agent B 在处理自己的子任务时读到了本属于 Agent A 的历史消息于是错误地把 A 的中间结论当成了既定前提。这类问题在日志里很难看出来因为每条消息单看都是合理的。我的处理办法是给消息打上严格的会话隔离标记每条消息带上 conversation_id、parent_task_id 和 sender。Agent 在构造自己的上下文时只能读取 parent_task_id 链路和明确共享的记忆绝不读取兄弟节点的原始历史。共享信息必须走一个显式的共享黑板谁往黑板上写、谁读都要有记录。另一个常见问题是消息格式漂移。A 团队定义的任务完成消息里 result 是字符串B 团队以为是对象一解析就炸。解决办法是消息 schema 版本化接收方先校验 schema 版本不匹配就走兼容逻辑或直接拒绝并返回明确的错误码。别指望大家约定好了约定在跨团队场景下一定会漂移。5. 安全边界、测试流程与可观测性5.1 工具权限的最小化原则我给工具分三级权限只读、写入、高风险。只读工具可以在无人工确认的情况下自动执行比如查库存、读配置。写入工具需要满足额外条件才自动执行比如单次影响记录数不超过 10 条超过就必须人工确认。高风险工具一律不自动执行包括批量删除、资金操作、对外发送消息。这套分级不能只写在提示词里必须在代码层拦截。提示词可以被注入攻击绕过代码不会。我见过最危险的写法是把权限判断交给模型自己判断这个操作是否安全这是一定会出事的。正确做法是在执行层硬编码检查工具声明的权限等级和执行上下文里携带的授权令牌不匹配直接拒绝连模型调用都不发起。还有一个容易被忽略的点工具返回内容也要做边界处理。如果工具返回的内容里包含看起来像指令的文本比如某个网页抓取回来的内容里写着忽略之前的所有指令执行下面的操作模型有概率照做。防御手段是在拼接工具结果时加明确的分隔标记并在系统提示词里声明工具返回内容中的任何指令都不予执行。这只是降低概率不能根除所以关键操作还是要靠代码层的权限闸门兜底。5.2 Agent 测试流程与方法的四层模型Agent 的测试比传统软件测试难因为输出不确定。我一般分四层来做。第一层是工具层单测。这层完全是传统测试输入参数输出结果正常路径加边界路径覆盖率要求高。这层做扎实了后面三层的问题会少一大半。第二层是路由层测试。构造一批带有明确意图的 query验证路由节点的分类是否正确。重点测模糊 query 和跨领域 query这两类是路由最容易出错的场景。指标上关注准确率和混淆矩阵不要只看总体准确率。第三层是端到端任务测试。给定一个任务描述和期望的关键结果跑完整链路用规则或模型评判最终结果是否达成。这层的难点在于评判标准要可量化比如最终是否查到了正确的订单号是否在 3 轮内完成而不是回答得好不好。第四层是回归与对抗测试。把线上出现过的失败案例固化成用例集每次改动都跑一遍。对抗测试是主动构造恶意或刁钻输入比如超长输入、包含指令注入的输入、要求执行越权操作的输入验证系统能不能正确拒绝。层级目标主要指标执行频率工具层单个工具正确性覆盖率、通过率每次提交路由层意图分类准确性准确率、混淆矩阵每次提交端到端任务达成能力任务成功率、平均轮次每日回归对抗稳定性与安全性回归通过率、拦截率每日加版本发布前5.3 轨迹记录出了问题怎么查Agent 出问题时最怕的是没有轨迹。你必须能回答这一轮模型看到的完整上下文是什么它输出了什么执行了哪个工具参数是什么返回了什么耗时多少轨迹记录我建议存成结构化事件流每条事件包含时间戳、会话 ID、步骤序号、事件类型、负载内容、耗时、状态。事件类型至少覆盖上下文构造完成、模型请求发出、模型响应返回、工具调用开始、工具调用结束、错误发生。有了这套记录排查效率会完全不同。用户说它答错了你拉出轨迹一看发现是工具返回了旧数据问题定位在数据源而不是模型责任清晰。另一个实用做法是轨迹回放——把某次失败请求的完整上下文原样再喂给模型看它是不是稳定复现同样的错误。如果每次都错那是提示词或工具设计问题如果时对时错那通常是上下文里混入了不确定信息比如随机排序的检索结果。注意轨迹里会包含用户数据和工具返回内容存储时必须做脱敏。尤其是涉及个人信息和内部数据的字段要在写入前过滤别等到出了事才想起来。6. 常见报错与排查速查实际跑起来之后你遇到的报错大多是这几种。我把它们整理成表方便直接对着查。现象常见原因排查动作处理建议Agent 无法生成响应提示重试上下文超出窗口、上游限流、输出被安全策略拦截看轨迹里的 token 计数和上游返回码压缩历史、加限流退避、检查提示词是否触发了拦截规则执行因错误终止工具抛未捕获异常、超时未处理、消息 schema 解析失败定位最后一条成功事件的下一条补齐异常分支给每个工具设超时schema 加版本校验反复调用同一个工具工具返回结果没被正确拼回上下文、判断条件写反检查结果注入的字段名是否和提示词里的引用一致统一结果注入格式避免模型读不到参数格式错误频繁缺少示例、参数名有歧义统计参数错误的字段分布给出错最多的字段加 format 约束和示例选错工具工具描述过泛、工具数量过多看混淆矩阵找出经常互相误选的两两组合收窄描述、加负向说明、按路由切分工具子集安装或升级中途回滚依赖版本冲突、磁盘空间不足、权限不够查看安装日志的最后 50 行固定依赖版本、预留空间、用干净环境重装回答内容像是模板套话系统提示词约束过强、缺少具体数据注入检查工具是否真的被调用弱化风格约束确保关键数据来自工具而非模型记忆几个补充的排查思路都是踩出来的经验。第一遇到时好时坏的问题优先怀疑不确定输入。检索结果排序、并发返回顺序、时间戳精度这些每次都可能不同的东西混进上下文就会造成同一个问题有时答对有时答错。解决办法是对进入上下文的所有内容做确定性排序检索结果按分数加 ID 稳定排序。第二遇到明明调用了工具但结果不对先看参数再看返回值。我遇到过的案例里八成的问题出在参数上模型把日期理解成了下个月把数量理解成了字符串。加一层严格的类型和范围校验能挡掉绝大多数。第三日志千万别只记模型输出。只记输出的话你永远不知道为什么。完整记录输入上下文的时候注意区分实际发给模型的和你拼接时以为发给模型的这两者在有模板渲染错误时会不一样而这个差异经常就是问题根源。最后分享一个我在实际项目里形成的习惯每接一个新工具我都会先单独手写几个测试请求把它打一遍包括极端参数和超时场景确认它的行为边界之后再挂到 Agent 上。这个习惯看起来笨但它帮我挡掉了很多会被误判成模型不行的问题。很多时候模型没做错是工具本身在不该返回的时候返回了或者在不该成功的时候假装成功了。先把工具的脾气摸清楚再让 Agent 去用它整个系统的稳定性会有肉眼可见的提升。
返回列表