Agent-Reach:让智能体真正触达工具、上下文与协作者的接入层架构

发布时间:2026/9/18 12:14:39
Agent-Reach:让智能体真正触达工具、上下文与协作者的接入层架构 1. Agent-Reach 想解决的那个手够不着的问题第一次把智能体接到真实业务上的人几乎都会经历同一个瞬间的落差在对话框里它引经据典、逻辑清晰一旦让它去查一张真实的订单、改一条真实的数据、拉一个真实的报表它就开始编造接口名、杜撰字段、假装调用成功。这不是模型变笨了而是它的手根本没伸出去。Agent-Reach 这个项目做的就是把这只手伸出去的事——让智能体能够真实地触达外部工具、真实地触达历史上下文、真实地触达另一个智能体。我把这个能力的三个方向概括成一句话工具可达、上下文可达、协作者可达。工具可达解决能不能碰外部系统上下文可达解决记不记得住之前发生的事协作者可达解决能不能把活儿分给别的智能体。这三件事听起来是三个独立模块但在工程实现上它们共享同一套底座——一个把外部能力抽象成统一描述、统一鉴权、统一编排的接入层。这套接入层就是 Agent-Reach 的核心。写这篇文章的出发点很直接市面上的智能体教程大多停在怎么让模型输出一段 JSON这一步而从能输出 JSON到能在生产环境里稳定跑三个月中间隔着一大堆没人愿意细讲的脏活。这篇文章适合两类人看一类是刚接触智能体开发、正在搭第一个能用的项目的人另一类是已经把 Demo 跑通、现在被超时、重试、越权、上下文爆炸折磨得睡不着的人。前者可以照着抄骨架后者大概会在中间几章找到共鸣。需要先说明一点Agent-Reach 这个名字听着像一个开源框架实际上它更像一种架构取向——你怎么划分智能体自身的循环和外部接入层之间的边界怎么定义工具和状态的所有权归属。所以我不打算把它包装成一个装完就能用的库那种东西往往在第二个需求变更时就崩了。我更想讲清楚的是分层思路和踩过的坑具体代码你按自己的技术栈写就行。1.1 从一个具体的失败场景说起我印象最深的一次事故是给一个内部知识助手接了工单系统的查询能力。Demo 阶段一切正常上线第二天开始出现间歇性查不到工单。排查了半天发现问题出在三个地方叠加第一模型有时会把工单号里的字母 O 识别成数字 0而我们的查询接口对格式做了严格校验直接返回空列表而不是报错第二工具描述里没写清楚工单号格式为两个字母加八位数字模型只能靠猜第三接口超时时间设成了 30 秒而工单系统在高峰期响应要 40 秒超时后我们的重试逻辑又没有做幂等保护导致偶尔触发重复查询。三个问题拆开看都很低级叠在一起就变成了一个偶发、难复现、用户已经失去信任的线上事故。这件事让我彻底改变了对智能体工程的认知决定智能体能不能用的从来不是模型推理能力而是接入层够不够结实。模型再聪明工具描述写错了它也只能瞎猜推理再准确超时没兜住它也只能报错。Agent-Reach 的第一版需求文档就是把这些非模型问题一条条列出来当验收标准。工具描述必须包含参数格式约束、必须包含失败时的错误语义、每个写操作必须声明幂等性、每个外部调用必须有独立的超时和降级策略。这些要求本质上和模型无关和任何一个后端服务对接第三方系统的要求是一样的但偏偏在智能体项目里最容易被忽略因为大家默认模型会搞定的。1.2 Reach 的三层含义与它们的耦合点把触达拆成三层之后我发现很多设计矛盾其实来自层与层之间的耦合没理清。工具可达是最外层指的是智能体能不能调用外部 API、数据库、文件系统、浏览器。这一层的核心资产是工具schema——用一份结构化的描述告诉模型有这么个能力、参数长这样、什么时候该用。上下文可达是中间层指的是智能体在处理当前任务时能不能拿到相关的历史信息。这一层最容易被误解成把上下文窗口开大就行实际上窗口越大噪声越多模型注意力越分散效果反而可能更差。协作者可达是最内层指的是智能体能不能把子任务交给另一个专门化的智能体。这一层的前提是前两层已经做好——一个连自己工具都调不明白的智能体交给它再多的协作者也没用。三层的耦合点在于状态。工具调用会产生状态比如查到的订单数据上下文检索会引入状态比如历史对话摘要多智能体协作会传递状态比如上游智能体的中间结论。如果状态的所有权没有明确归属就会出现经典的三个模块都在写同一份 memory互相覆盖的问题。我在第二版重构时做的最大改动就是把状态收敛到一个明确的会话状态机里工具、记忆、协作者都只能通过它提供的接口读写不能直接碰底层存储。1.3 为什么它不该是又一个智能体框架每次有人问我要不要用某个现成的智能体框架我的回答都是同一句先看它把 harness 放在哪一层。框架的价值在于帮你处理循环控制、消息路由、状态持久化这些通用问题框架的代价是你会被它的抽象绑住一旦遇到它没考虑到的场景你要么绕过去写一堆丑陋的适配代码要么改它维护一个私有分支。我见过太多团队在这个岔路口选错方向最后项目变成给框架打补丁而不是做业务。Agent-Reach 的定位是接入规范而不是运行框架。它规定的是工具怎么声明、记忆怎么组织、协作者怎么互相发现但不规定你用什么样的循环去驱动模型。你可以用最朴素的手写 while 循环也可以用现成的编排引擎只要接入层遵守这套规范上层怎么换都不影响。这个选择带来的最大好处是——模型换代的时候你几乎不用改代码。半年时间里我们把底层模型换了两次接入层的工具定义一行没动只是重新调了一遍提示词和参数。2. Harness 与 Agent 的边界到底划在哪要讲清楚 Agent-Reach 的架构落点绕不开一组被问烂了的概念harness 和 agent 有什么区别skill 和 agent 又有什么区别。这几个词在社区里被混着用导致讨论架构时经常鸡同鸭讲。我按自己的理解给一套划分方式不追求和谁的定义对齐只追求能指导写代码。2.1 Harness、Agent、Skill、Tool 四个词的分工我倾向于这样区分Tool工具一个具体的能力单元有明确的输入输出契约。比如查询订单详情这个函数。它不知道自己在被谁调用也不关心上下文。Skill技能一组工具加上一段使用说明的封装对应一类任务。比如售后处理这个技能里面包含查订单、查物流、发起退款三个工具以及一段先查订单再判断是否在售后期内的指引。Agent智能体一个有目标、能自主决定下一步做什么的执行体。它拥有一个循环观察状态、决定动作、执行、观察结果、继续。Harness承载层包在智能体外面的一整套基础设施负责模型调用、消息序列化、工具调度、超时重试、日志埋点、权限校验。它不决定做什么只负责怎么把动作可靠地执行下去。用一句话概括它们的关系harness 是跑道agent 是跑者skill 是训练科目tool 是跑鞋。这个类比不严谨但足够直观。这个划分最重要的实践意义在于harness 里的逻辑应该是确定性的、可测试的agent 里的逻辑应该是模型驱动的、需要评测的。如果你发现自己在一个 if-else 里疯狂判断模型这次输出的是不是这个意图那你多半是把本该放在 harness 的确定性逻辑错误地交给了 agent。反过来如果你在 harness 里写了一长串如果用户说了 A 就先做 B那说明你在用代码硬编码一个本该由模型判断的决策树。2.2 循环控制权归属决定了架构形态判断一个智能体项目架构好不好我有个很偷懒的方法看它的主循环写在哪、谁掌握转场条件。常见的三种形态形态循环控制适用场景典型问题代码编排由代码写死步骤顺序流程固定、步骤少、容错要求高灵活性差新增分支要改代码模型自主由模型决定下一步调什么工具任务开放、路径不确定容易绕圈、成本不可控混合模式代码定骨架模型填细节大多数真实业务边界划分需要设计功力Agent-Reach 走的是混合模式。外层用一个明确的状态机管住任务处在哪个阶段每个阶段内部再放开让模型自主选择工具。这样做的好处是成本和可控性都能兜住状态机保证了不管模型怎么发挥任务总归会往前走或者明确失败不会无限循环阶段内的自由又保留了模型处理复杂情况的能力。我踩过一个很典型的坑值得展开说。早期版本我们把整个任务全权交给模型结果出现了一个诡异的现象处理退款申请时模型有时候会先去查物流、有时候先去查订单、有时候甚至先去查用户画像路径完全不固定。单次成功率还行但P99 延迟高得离谱因为偶尔它会连查七八个工具才找到关键信息。后来改成混合模式状态机规定退款流程第一步必须先查订单状态一下子把尾部延迟压下来了成功率反而还提升了几个点。这个经验告诉我给模型自由但要给在正确的抽象层级上。2.3 Agent-Reach 在分层里的具体落点现在可以画出 Agent-Reach 的位置了。它不属于 harness也不属于 agent而是横跨两者的一条契约层┌─────────────────────────────────────┐ │ Agent 层目标、决策、循环 │ ├─────────────────────────────────────┤ │ Agent-Reach 契约层 │ │ · 工具 schema 规范 │ │ · 记忆读写接口 │ │ · 协作者发现与调用协议 │ │ · 权限与审计钩子 │ ├─────────────────────────────────────┤ │ Harness 层模型调用、调度、重试 │ ├─────────────────────────────────────┤ │ 外部系统API、数据库、文件、浏览器 │ └─────────────────────────────────────┘这条契约层的存在意义是让 agent 和 harness各说各话也能对接上。Agent 只需要知道有这么个能力可以调不需要知道它是 HTTP 还是 gRPCHarness 只需要知道要执行这个调用不需要知道模型为什么选它。当你要把某个工具从 REST 换成消息队列时改的只有契约层下面的适配器上面的提示词和状态机完全不动。实际写代码时我建议契约层就用最朴素的数据结构表达——一个字典或者一个类字段固定、可序列化。不要一上来就搞抽象基类继承体系那种设计在智能体项目里往往会变成负担因为工具形态变化太快继承层次根本跟不上。3. 工具可达把外部系统接进来时最容易翻车的细节工具接入看起来是最没有技术含量的一环——不就是写个函数然后告诉模型吗但实际项目中这一环出的问题占了线上事故的一大半。我把踩过的坑按出现频率排了个序从工具描述开始讲。3.1 工具描述写不好模型就会开始编模型选不选对工具几乎完全取决于描述。我总结了三条硬规则都是从事故里换来的。第一参数描述里必须写格式约束和示例。订单号这种描述是不够的要写成订单号格式为两个大写字母加八位数字例如 AB12345678注意数字 0 和字母 O 不要混淆。后面半句听起来很蠢但它实实在在降低了一类错误。模型不会因为你写清楚格式就绝对不犯错但它犯错之后的自我纠正能力会明显提升。第二工具描述里要写什么时候不该用。这条很少有人做。比如查询订单这个工具如果只写用于查询订单信息模型在面对退款请求时也可能先调它。加上一句本工具仅返回订单基础信息不包含物流和退款状态如需这些信息请调用对应工具能显著减少无效调用。这本质上是在给模型做负样本示例。第三失败返回值要带语义不能只返回空。前面提到的工单查询事故根因就是接口查不到时返回了空列表[]。模型看到空列表无法区分这个工单号确实不存在和你的工单号格式错了于是它要么继续瞎猜要么编一个答案。改成返回结构化错误之后模型能立刻意识到是格式问题并重新询问用户。一份合格的描述大概长这样{ name: query_order_detail, description: 根据订单号查询订单基础信息包括下单时间、金额、商品列表、当前状态。仅返回基础信息不包含物流轨迹和退款进度。当用户询问订单号相关查询时使用当用户提出退款、退货请求时不要直接使用本工具应先走售后判断流程。, parameters: { order_no: { type: string, description: 订单号格式为两个大写字母加八位数字例如 AB12345678。输入前请确认是否把数字 0 误识别为字母 O。 } }, returns: { success: 订单对象含 status 字段pending/paid/shipped/completed/cancelled, not_found: 订单不存在通常意味着订单号错误应向用户确认, invalid_format: 订单号格式不合法请检查后重新输入 }, idempotent: true }3.2 幂等、超时与重试三个必须显式声明的字段这三个字段在我的规范里是强制项没有就先不给上线。幂等性决定这个工具能不能被安全重试。查询类工具天然幂等重试没有副作用创建工单、发起退款这类写操作不幂等重试可能造成重复扣款或者重复建单。对于不幂等的工具我在契约层强制要求传入一个由调用方生成的请求 ID服务端用它做去重。这一步花不了多少时间但能避免最难排查的那类事故。超时时间我不建议用统一值。查询类给 5 到 8 秒复杂计算类给 20 到 30 秒写操作给 10 秒。设统一值的后果是要么激进导致大量误超时要么保守导致模型等待过久、整个任务卡住。更重要的是超时必须和用户感知对齐——如果一个工具平均要 25 秒而你告诉用户稍等一下用户的耐心大概只够 10 秒那这个工具本身的设计就该重新考虑比如改成异步任务加轮询。重试策略要区分错误类型。网络抖动、502、连接重置这类瞬时错误重试 1 到 2 次是合理的参数错误、权限不足、资源不存在这类逻辑错误重试一万次也没用只会浪费时间和 token。我用一个简单的分类函数把错误码映射成retryable和fatal两类只有前者进重试队列。这个分类表是项目里最不起眼但最省事的文件之一。3.3 危险操作的闸门设计工具接入做到后面一定会遇到这个问题有些工具能删数据、能发消息、能扣款绝对不能让它被模型自由调用。我的做法是三道闸门。第一道是工具可见性。危险工具默认不出现在模型的工具列表里只有进入特定状态机阶段才动态注入。比如退款流程走到执行退款阶段execute_refund这个工具才会出现在候选列表里。这样模型在别的阶段根本没有机会误调它。第二道是参数校验。在契约层做一次硬校验金额上限、单日次数上限、目标对象是否在白名单里全部用代码判断不依赖模型自觉。我见过有人把单笔不超过 5000写进提示词就当成了限制这是非常危险的——提示词是建议代码才是约束。第三道是人工确认。超过阈值的操作走人工审批模型生成一个待确认的请求进入队列等处理。这一步会让流程变慢但对于不可逆操作来说慢一点总比错了强。提示三道闸门要按可见性 → 校验 → 确认的顺序设计不要颠倒。先做参数校验再做可见性控制是常见错误因为不可见的工具根本不该产生参数先去校验它只是在浪费一次调用。4. 上下文可达记忆不是把东西全塞进去上下文这一层我犯过的最大错误是把它当成存储问题。实际上它是信息筛选问题——从海量历史里挑出对当前决策真正有用的那一小部分。存储做得多好筛选做不好效果一样烂。4.1 短期、长期、工作三种记忆的分工我把记忆分成三类每类的生命周期和读写方式完全不同。记忆类型存什么生命周期读写方式短期记忆当前会话的原始消息单次会话结束后归档全量保留按窗口截断工作记忆当前任务的结构化中间状态单个任务显式读写状态机维护长期记忆跨会话的用户偏好、历史结论长期检索召回按需注入三者最容易出问题的是工作记忆。很多人图省事把中间状态也用自然语言塞进对话历史里结果就是模型每次都从一大段文本里重新解析结构。正确做法是用一个明确的结构化对象存状态只在需要的时候把它渲染成文本给模型看。这样做的另一大好处是——状态可以被代码直接检查比如如果工作记忆里已经有订单信息就跳过查询步骤这种判断用代码做是零成本的用模型做又要多花一次调用。长期记忆的检索召回我在下一节展开讲因为坑最多。4.2 检索召回相似度不等于有用向量检索最朴素的用法是拿用户当前问题去查最相似的 N 条历史记录。这个方法在有明确事实型问题的场景下还行在多轮任务型场景下经常帮倒忙。我遇到过这样一件事用户先问了 A 产品的退货政策处理完之后又问 B 产品。检索系统把 A 产品的退货政策召回了因为文本相似度极高。模型于是把 A 的政策混进了 B 的回答里。这个错误的根源是检索只看了语义相似度没看时效性和实体一致性。后来我加了两层过滤时间衰减和实体约束。时间衰减很好理解越久远的历史权重越低超过一定时间的直接不召回。实体约束是如果当前对话明确提到了某个产品 ID那么召回结果里实体不匹配的直接丢弃。这两层加完错误率明显下降而且召回条数也从 8 条降到 3 条注入的上下文短了一大半模型的响应速度和准确率都变好了。另外一个反直觉的经验是召回条数不是越多越好。我做过一轮对比召回 3 条、5 条、10 条三组结果是 3 条的综合表现最好。多出来的内容大多是噪声模型需要花注意力去分辨这条和我现在的问题到底有没有关系反而稀释了真正有用的信息。4.3 上下文预算怎么分配上下文窗口是有限的资源分配原则我总结成一句话当下最需要的信息给最多历史信息给最少工具描述给固定的量。我的默认分配大概是这样的系统提示词和工具描述占 15% 到 20%当前任务的原始输入占 10%工作记忆占 15%检索召回的历史占 20%留给模型输出和推理过程 35% 左右。这个比例不是拍脑袋来的是观察了很多次任务失败时上下文被谁占满了之后调出来的。最容易被挤爆的是工具描述。工具一多光描述就能吃掉一半窗口。解决办法有两条一是按阶段动态注入工具子集二是把不常用的工具描述压缩成一句话摘要需要时再展开。我用的是前者效果更直接——在查订单阶段只给模型 3 个相关工具它做选择的速度明显变快。还有一个小技巧值得分享在系统提示词里明确告诉模型不确定时要主动说不知道同时把工具调用的失败信息原样透传给它。很多幻觉不是模型想编而是它不知道该说什么只能顺着语气往下接。给它一个明确的承认失败的出口编造行为会明显减少。5. 协作者可达多智能体不是越多越好单智能体能搞定的事别拆成多智能体——这是我在这个方向上交了最多学费之后得出的结论。5.1 什么时候多智能体是负优化先说什么时候该用。我的判断标准是三条同时满足才考虑拆分子任务有独立且稳定的输入输出、子任务需要不同的工具集或不同的提示词人格、子任务的执行时间足够长值得并行。反过来如果两个角色之间需要高频来回确认那拆开只会让消息传递成本爆炸。我做过一个对比实验把一个分析需求 写代码 写测试的任务拆成三个智能体结果总耗时比单智能体串行做还多了 40%。原因在于每次角色交接都要重新注入上下文而且上游表达的模糊之处下游理解不了来回澄清消耗了大量轮次。拆分的收益主要来自两处上下文隔离和并行执行。上下文隔离指的是一个只做代码审查的智能体不需要知道需求讨论的全部历史它只需要看最终代码和评审标准这样它的注意力更集中评判更稳定。并行执行指的是几个互不依赖的子任务同时跑墙钟时间自然缩短。5.2 能力发现Agent Card 这类描述文件的价值多智能体协作的第一个工程问题不是通信而是发现——A 怎么知道 B 能干什么。早期我们用的是硬编码调用方直接写死要调 reviewer 这个智能体。这种方式在角色少的时候能用角色一多就崩了因为新增一个角色要改所有调用方。后来改成能力描述文件的形式每个智能体对外暴露一份声明写明自己叫什么、擅长什么、输入需要什么格式、输出会返回什么结构、有哪些限制条件。调用方不再指定具体是谁而是描述我需要一个能做的事由路由层根据声明去匹配。{ name: code_reviewer, capabilities: [static_analysis, style_check, security_scan], input: { code_diff: string统一 diff 格式的代码变更, language: string编程语言标识 }, output: { issues: 数组每项含 severity/line/message/suggestion }, limits: { max_diff_lines: 2000, timeout_seconds: 120 } }这份声明看起来简单但它解决了三个实际问题路由可以自动匹配、调用方可以提前校验参数是否符合约束、超时可以在路由层统一设置。特别是第三点如果每个调用方自己设超时一定会有人漏设然后卡死整个流程。注意能力声明里的限制条件必须和实际实现保持一致。我见过声明写最多 2000 行实际只能处理 500 行的情况结果是调用方按声明传了 1500 行服务直接崩掉。声明文件要么自动从实现里生成要么在 CI 里加一个校验步骤靠人肉同步一定会出错。5.3 消息传递与状态同步的实际做法多智能体之间的通信我强烈建议不要做自由对话。让两个智能体用自然语言互相聊天听起来很酷实际跑起来会出现角色混淆、话题漂移、无限寒暄这些问题。我的做法是定义有限的消息类型每种类型有固定的结构。常见的有任务下发、结果返回、澄清请求、错误上报、进度通知。每种消息都有明确的发送方和接收方角色不能反过来用。这样虽然牺牲了一些灵活性但换来的是可测试——每条消息都可以被单元测试覆盖这在多智能体系统里非常宝贵。状态同步的原则是单一数据源加只读副本。共享的任务状态只有一份维护在协调者手里各个子智能体拿到的是快照改完之后把差异提交回协调者由协调者决定是否合并。我不用共享内存或者共享数据库连接的原因很简单——并发写入冲突的排查成本太高了而在智能体场景里绝大部分冲突是可以通过让协调者串行决定来避免的。实际落地时还有一个细节容易被忽略中间结果的格式必须校验。子智能体返回的结果不能直接信任协调者要按声明的输出结构做一次校验缺字段或者类型不对就当作失败处理。这一步救过我很多次因为模型输出的 JSON 偶尔会多包一层或者少个字段不校验的话错误会一路传播到最终结果里排查起来极其痛苦。6. 怎么证明 Reach 真的伸过去了智能体项目的评测和传统软件测试最大的区别是同样的输入输出可能不同。这决定了你不能用传统的断言式测试得换一套思路。6.1 评测集怎么造才不像摆设我见过很多团队的评测集是从文档里抄的一些示例问题跑分很好看上线之后一塌糊涂。问题出在评测集和真实分布不匹配。我的做法是从真实日志里采样。每次上线后收集一批真实请求人工标注出期望的行为路径注意是路径不只是最终答案然后按错误类型分层抽样组成回归集。这样做的好处是评测集天然带有真实数据的脏——错别字、口语化表达、信息缺失、多意图混杂这些恰恰是模型最容易翻车的地方。评测的判定方式我分了三个层次从松到严结果正确性最终答案是否符合预期这是最基础的。路径合理性有没有调用不该调的工具、有没有出现明显绕路、有没有超过最大轮次。过程质量参数是否准确、错误处理是否恰当、中间回复是否让用户困惑。只测第一层是不够的。我遇到过一个案例最终答案对了但过程中调了六次工具其中三次是无效重试成本是正常情况的三倍。如果不测路径这种问题永远不会被发现。6.2 失败案例的分类方法评测跑出来的失败案例如果不做分类你就只能看到一堆不好用的模糊感受。我用的分类维度是这样的失败类型典型表现责任层工具选择错调了功能相近但不对的工具工具描述参数错误格式对不上、漏传必填项参数描述 校验幻觉输出编造不存在的数据提示词 上下文循环绕路反复调同一个工具状态机 轮次限制上下文丢失忘了前面确认过的信息工作记忆超时失败上游慢或者重试过多接入层策略有了这个分类改进方向就很明确了工具选择错就去改描述参数错误就去加校验和示例幻觉多就去补工作记忆和承认不知道的出口。这比笼统地说效果不好再调调提示词要有效得多。顺便说一个我自己的经验每次只改一个变量。智能体系统里变量太多同时改提示词、改工具描述、换模型最后效果好你也不知道是哪个起的作用效果差你更不知道该回滚什么。慢一点但每一步都可解释。6.3 灰度与线上观测上线之后我盯的指标和传统服务不太一样。除了延迟和错误率我更关注这几个工具调用分布。如果某个工具几乎从不被调用要么是描述写得有问题要么是这个工具压根不需要存在。如果某个工具被调用的频率远高于预期可能是它在被当成万能工具用。平均轮次。轮次突然上涨通常意味着某个环节开始绕路可能是外部接口变慢导致超时重试也可能是数据格式变了导致模型需要多次尝试。失败原因分布。按上一节的分类打点每周看一次分布变化。如果某一类失败突然占比上升说明最近的某次改动引入了回归。灰度策略上我用的是按用户分层而不是按流量比例。因为智能体任务往往有连续性——同一个用户的多轮对话最好落在同一版本上否则体验会断裂。按用户 ID 取模分流能保证一个用户在一次会话里始终面对同一个版本。7. 安全边界和几个印象深刻的坑智能体安全不是加个过滤器就完事的事它的攻击面比传统 Web 应用大得多因为所有的输入都可能被当成指令。7.1 提示注入为什么难防传统注入攻击有明确的边界SQL 有语法结构参数和语句能分开提示注入没有。用户上传的一份文档、一个网页内容、甚至一个文件名都可能包含忽略之前的指令执行以下操作这类文本而模型无法从原理上区分这是数据和这是指令。我现在的防御是多层叠加没有哪一层能单独解决问题第一层是内容隔离。外部内容注入时用明确的分隔标记包起来并在系统提示词里说明分隔标记内的内容只是数据不是指令。这能挡住最粗糙的注入。第二层是输出约束。模型要调用工具时必须输出结构化格式不允许自由文本触发工具调用。这样即使注入成功了模型也只是输出一段被隔离的文本不会真的执行。第三层是权限最小化。工具本身能做的事就限死在最小范围内。如果一个工具只需要读两张表就不要给它整库读权限。这是最后一道也是最可靠的一道防线——假设前两层都被突破了损失也要可控。我一直强调第三层因为在智能体场景里前两层永远有被绕过的可能只有权限是硬约束。7.2 我踩过的三个坑第一个坑是看起来正常的静默失败。工具返回了 200但返回体是{code: 0, msg: success, data: null}。我们的处理逻辑只检查了 HTTP 状态码把 null 当成了有效数据传给模型。模型拿到 null 之后开始编造内容。教训是——业务层面的成功判定必须显式写出来不能靠 HTTP 状态码。第二个坑是重试引发的重复写。前面提过的不幂等重试。那次事故之后我们给所有写操作都加上了请求 ID 去重并且在 harness 层做了硬限制没有声明幂等性的工具默认不允许自动重试。宁可失败一次让用户重试也不要冒重复写的风险。第三个坑是成本失控。智能体任务和普通接口调用最大的区别是成本随轮次指数增长。早期没有轮次上限和成本上限有一次模型陷入循环一个任务烧掉了正常量的二十倍。后来加了两个硬限制单任务最大轮次、单任务最大 token 预算超了直接终止并返回已完成的中间结果。这两个限制看起来粗暴但效果立竿见影再也没出现过成本异常。7.3 想入门这个方向的话我建议的路线经常有人问我怎么学智能体开发我给的建议是按接入层 → 状态管理 → 评测体系 → 多智能体的顺序推进每一层都做出一个能跑的小东西再往上走。先做一个只有一两个工具的智能体把工具描述、错误处理、超时重试这三件事做扎实。这一步看起来简单但能让你体会到绝大部分工程问题。然后再加记忆先做工作记忆结构化状态再做检索召回感受一下上下文噪声对效果的影响。接着搭一个最小的评测流程哪怕只有二十个用例也比没有强。最后再去碰多智能体而且要带着一个明确的问题去——我遇到了什么瓶颈才需要拆而不是多智能体听起来更高级。我在这个方向上花的时间大概有六成在工具接入和错误处理上两成在评测和调试上真正花在设计提示词和选模型上的不到两成。这个比例和大多数人的预期是反的但如果你真的在做一个要上线的智能体项目大概率也会是这个分布。把工具描述写清楚、把超时和重试兜住、把幂等和权限管好这些事不性感但它们才是决定一个智能体能不能真正触达外部世界的分水岭。