
Agent-Reach 这个词第一次在我脑子里成型是在一个挺尴尬的深夜调试现场。项目里的 agent 编排部分写得漂漂亮亮任务拆解逻辑清晰得像教科书可一旦让它去真正碰外面的世界——调一个接口、读一个文件、点一下浏览器里的按钮——整个链路就开始散架该调的工具没调参数传歪超时之后卡死在那里不吭声最后只丢回来一句 agent couldnt generate a response 之类的报错。规划层明明没问题问题全出在规划和真的做到之间那一段薄薄的、却异常关键的距离上。Agent-Reach 想干的就是补上这段距离。说得具体一点Agent-Reach 是一套给 agent 加装触达能力的设计思路与实现结构它管的是 agent 怎么知道自己有哪些外部能力可以用、怎么在众多工具里挑对那一个、怎么把参数安全地送出去、怎么接住返回结果、怎么在失败时收场。它不负责让模型变聪明它负责让模型变聪明之后真的有手有脚。这套东西适合三类人刚学完 agent 基础想动手搭项目的人、被工具调用不稳定折磨过的 agent 开发同学、以及在做多 agent 协作和工业级 agent harness 的工程师。哪怕你目前只会写最简单的 prompt 调用看完也能把骨架搭起来。1. 先把 Agent-Reach 这个概念钉死很多人一听到触达就理解成能联网就行这个理解太窄了。触达在 agent 语境里是个完整的闭环它至少包含四件事发现能力、选择能力、执行能力、回收结果。少任何一环agent 都会表现得像个记性不好的实习生——知道该干什么但就是干不成。我在早期版本里犯过的最大错误就是把给模型塞一堆工具定义当成了触达。结果模型在一堆长得差不多的工具里随机挑挑错的概率高得离谱。后来才明白触达层的核心工作量根本不在给而在管。1.1 它要解决的三个具体症状第一个症状是工具误选。当你有十几个工具其中三个都是查询数据类的模型很容易凭字面意思乱选。这不是模型笨是工具描述没写好、路由没设计好。我实测过一个极端案例把两个功能相近的工具放在一起命名只差一个词误选率能从 8% 蹿到 40% 以上。第二个症状是参数漂移。模型生成的参数看着像对的实际少了必填项、类型不对、或者把日期写成了下周三这种人类话术直接丢给接口。触达层的责任是在参数离开 agent 之前把它校验和规范化而不是把烂参数丢给下游然后收获一个 500 错误。第三个症状是失败无回声。工具调用超时了、返回了非预期格式、或者执行到一半异常终止如果没有兜底逻辑整个 agent 链路就断在那里用户看到的就是一句莫名其妙的执行错误。触达层必须保证每一次触达都有明确的结局成功、可重试的失败、或者明确的放弃理由。1.2 Agent、Skill、Harness、Reach 四者的边界这几个词最近被混用得厉害agent 面试和 agent 八股里也常考。我用一句人话概括agent 是谁在做skill 是会做什么harness 是怎么被管着做reach 是怎么把手伸出去做。它们不是替代关系是同一套系统里的不同切面。概念回答的问题典型产物缺失后的表现Agent谁在决策规划循环、提示词、模型配置没有自主性只能被动响应Skill会哪些本事领域知识包、流程模板什么都会一点什么都不精Harness怎么被约束和观测生命周期管理、日志、权限、限流跑飞了没人知道出事没法查Reach怎么触达外部世界工具注册表、路由节点、执行器想得到但做不到卡在最后一公里这张表我自己贴在工位上过一段时间。每次有人问我agent 和 skill 有什么区别我都拿这张表糊他脸上。真正值得强调的是 harness 和 reach 的关系。工业级 agent harness 解决的是把 agent 关进笼子里跑的问题而 reach 是笼子伸出去的那只机械臂。机械臂本身的可靠性、权限边界、错误处理是 reach 的活笼子什么时候开门、跑多久、跑几次是 harness 的活。两者耦合很紧但职责必须分清否则排查问题时你会分不清到底是编排错了还是执行错了。还有个常被问到的点java agent 和这里的 agent 是不是一回事。完全不是。java agent 是 JVM 层面的字节码增强技术属于另一个知识体系跟 ai agent 开发基本不搭边。面试里如果有人拿这个混淆你直接指出概念差异就行。2. 架构选型为什么把触达层单独拆出来我见过不少项目把工具调用逻辑直接写死在 agent 主循环里一开始确实省事三五个工具的时候毫无问题。但工具数量一旦过十个、调用方一旦从一个变成多个、需求一旦从跑通变成跑稳这种写法就会变成负债。2.1 拆层的三个理由与代价拆层的第一个理由是可替换。模型会换从一家换到另一家路由策略也要跟着变工具会加今天十个明天三十个执行器要能扛住。把这些变化点关在一个独立层里主循环就不用动。第二个理由是可观测。触达层是 agent 唯一跟外部系统握手的地方所有出入流量都从这里过。把埋点、日志、限流、熔断全放这一层你就能用一套统一的观测方案覆盖所有工具不用为每个工具单独写监控。第三个理由是可测试。触达层可以脱离模型单独测试给定一个工具名和一组参数能不能正确执行、能不能正确报错。这部分测试是确定性的跑起来快、结果稳是整个 agent 项目里最值得投入自动化测试的地方。代价也要说清楚。拆层会带来额外的抽象开销简单场景下会显得啰嗦层与层之间的数据契约需要维护改一处要同步改几处新人上手需要先理解分层学习曲线更陡。我的判断标准是工具数量超过八个或者有第二个调用方接入就该拆了。提示如果你现在还在验证想法阶段工具就两三个别急着拆层。先跑通等痛点真的出现了再重构比一开始过度设计要划算得多。2.2 六个核心模块与一次请求的完整流转我把触达层拆成六个模块这套划分在实践中比较耐用工具注册表所有可用工具的元信息中心包含名称、描述、参数契约、权限等级、超时配置。路由识别节点拿到用户意图后决定该走哪个或哪几个工具输出候选集和置信度。参数装配器把模型输出的松散参数按契约校验、补默认值、做类型转换。执行器真正发起调用负责超时控制、重试、并发。结果归一化层把各种奇形怪状的返回统一成内部结构方便模型理解。观测与审计记录每一次触达的输入输出、耗时、结果状态。一次完整请求的流转大致是这样用户输入进来路由识别节点先从注册表拉取可用工具清单结合当前上下文计算候选集选出最匹配的一个参数装配器拿模型给的参数去对照契约不合格就打回让模型重生成执行器拿到干净参数后发起调用带超时和重试结果回来后归一化成统一结构再交回主循环让模型继续推理。整个过程观测层在旁记录。你注意到没有这套流程里模型只负责两件事给参数、读结果。所有判断该不该信的活都由触达层扛着。这个分工很关键因为模型在这两件事上确实强在守规矩这件事上确实弱。3. 工具注册表Agent-Reach 的地基注册表看起来简单就是一张表。但表里每条记录的写法直接决定了后面路由的准确率。我在这上面踩的坑比在其他任何模块都多。3.1 工具描述怎么写才不会被模型误选最常见的错误是把工具描述写成给人看的文档摘要。比如查询订单信息人一看就懂但模型面对查询订单信息和获取订单详情两个描述时基本靠猜。好的工具描述要让模型能区分它和近邻工具。我总结的写法公式是动词 对象 触发场景 明确边界。举个例子差的写法查询订单信息好的写法根据订单号查询单笔订单的完整状态适用于用户明确给出了订单号、需要了解该订单当前进度时。不适用于按时间范围批量统计订单。第二段里不适用于这半句很值钱。负向描述能显著降低误选率我在多个项目里实测过加上明确的排除条件后相近工具的误选率能降一半左右。另外工具描述里不要出现内部术语、缩写和代号模型对这些词的语义锚定很弱。也不要写太长超过两百字之后信息密度反而下降模型会抓不住重点。3.2 参数契约的三种表达方式与取舍参数契约的表达有三种常见路线纯自然语言描述、结构化 schema、以及两者混合。纯自然语言最省事但校验只能靠模型自觉适合快速原型。结构化 schema 最严谨能自动生成校验逻辑但模型看到复杂嵌套 schema 时容易漏字段。混合方案是我现在的主力用 schema 定义类型和必填项用一小段自然语言补上这个字段什么时候该填、格式大概长什么样。这里有个细节值得单独说枚举值。如果某个参数只有几个合法取值一定要在契约里显式列出。我见过太多项目用自由文本描述格式可选,结果模型填了个不在支持范围内的值下游直接报错。把枚举列清楚等于把校验前置了。3.3 代码一个可用的注册表实现下面是我常用的注册表结构用 Python 写的思路可以平移到任何语言from dataclasses import dataclass, field from typing import Any, Callable, Literal RiskLevel Literal[read, write, dangerous] dataclass class ParamSpec: name: str type_hint: str required: bool True enum: list[str] | None None default: Any None description: str dataclass class ToolSpec: name: str summary: str # 一句话说明 when_to_use: str # 触发场景 when_not_to_use: str # 排除条件 params: list[ParamSpec] risk: RiskLevel read timeout_sec: int 15 idempotent: bool True handler: Callable | None field(defaultNone, reprFalse) class ToolRegistry: def __init__(self): self._tools: dict[str, ToolSpec] {} def register(self, spec: ToolSpec) - None: if spec.name in self._tools: raise ValueError(fduplicated tool name: {spec.name}) self._tools[spec.name] spec def get(self, name: str) - ToolSpec | None: return self._tools.get(name) def describe_for_llm(self, names: list[str] | None None) - str: items self._tools.values() if names is None else \ [self._tools[n] for n in names if n in self._tools] blocks [] for t in items: param_lines [] for p in t.params: flag 必填 if p.required else 可选 extra f取值限于 {p.enum} if p.enum else param_lines.append( f - {p.name} ({p.type_hint}, {flag}){extra}: {p.description} ) blocks.append( f- {t.name}: {t.summary}\n f 何时使用: {t.when_to_use}\n f 不适用: {t.when_not_to_use}\n f 参数:\n \n.join(param_lines) ) return \n.join(blocks)注意describe_for_llm这个方法。它把注册表里的元信息拼成给模型看的文本而不是直接把原始数据结构序列化扔过去。这一步的格式化质量直接影响模型的理解效果。我习惯在拼装时把何时使用和不适用都带上因为它们对区分近邻工具的作用最大。还有一个设计点risk字段。它不是给模型看的是给权限层看的。读操作直接放行写操作记日志危险操作走人工确认。这个字段在整个体系里的价值后面第 6 节会展开。4. 路由识别节点让 Agent 选对工具的关键路由识别节点是整个触达层里最容易被低估的模块。很多人觉得把工具列表丢给模型让它自己选就够了这在工具少于五个时确实凑合超过之后就不行了。4.1 三种路由实现路径的对比路径一模型直选。把全部工具描述塞进 prompt让模型输出工具名和参数。实现最简单缺点是工具一多prompt 变长准确率和成本都会恶化。路径二向量召回 模型终选。先给每个工具的描述做 embedding把用户意图也向量化召回 top-k 个候选再让模型在这 k 个里选。工具规模上百时这条路线明显更稳。这里的 embedding 和 llm 是两个不同层面的东西embedding 负责找相似llm 负责做判断别混着理解。路径三规则预路由 模型兜底。用关键词、正则或者意图分类器先把明显的情况拦下来比如用户说了图片就直接进画图工具链其余交给模型。响应快、成本低但要小心规则维护成本规则一多就变成新的负债。我现在的默认组合是路径二加路径三明显意图走规则其余走向量召回最后模型终选。工具少于十个的项目用路径一就够了别过度设计。4.2 阈值、候选集与置信度兜底路由必须有一个我不确定的出口。这是很多项目缺失的设计。如果召回的相似度都很低模型又在硬选那还不如直接告诉用户我没找到合适的工具要不要换个说法。我的做法是设两个阈值召回阈值和采纳阈值。召回阈值决定候选集大小通常设得低一些宁滥勿缺采纳阈值决定模型的选择要不要被采纳设高一些。当模型的输出置信度低于采纳阈值或者候选集为空就触发兜底流程。兜底流程也不是简单报错而是分三步先尝试把用户意图拆解成更小的子任务重新路由还是不行就返回澄清问题实在无法处理才返回明确的能力边界说明。这套下来用户体验比直接崩掉好太多。4.3 关于路由识别节点的代码实现一个精简版的路由节点长这样import math def cosine(a: list[float], b: list[float]) - float: dot sum(x * y for x, y in zip(a, b)) na math.sqrt(sum(x * x for x in a)) nb math.sqrt(sum(y * y for y in b)) return dot / (na * nb 1e-9) class Router: def __init__(self, registry, embed_fn, recall_k5, recall_thr0.35): self.registry registry self.embed_fn embed_fn self.recall_k recall_k self.recall_thr recall_thr self._tool_vecs: dict[str, list[float]] {} self._build_index() def _build_index(self): for name, spec in self.registry._tools.items(): text f{spec.summary} {spec.when_to_use} self._tool_vecs[name] self.embed_fn(text) def recall(self, query: str) - list[tuple[str, float]]: qv self.embed_fn(query) scored [(n, cosine(qv, v)) for n, v in self._tool_vecs.items()] scored.sort(keylambda x: x[1], reverseTrue) hits [(n, s) for n, s in scored[: self.recall_k] if s self.recall_thr] return hits_build_index在注册表变更时需要重建这一步别忘了做失效处理。我在早期版本里就是因为新增了工具但没重建索引导致新工具永远召回不到排查了两个小时才发现问题日志里什么异常都没有因为逻辑上确实没出错它只是不知道新工具存在。5. 从零搭一个最小可运行的 Agent-Reach理论说够了来动手。这一节我会给出一个能真正跑起来的最小实现包含环境准备、执行循环、记忆管理和外部系统接入四个部分。5.1 环境准备与依赖选择最小依赖其实很少一个模型调用客户端、一个向量化方案、一个 HTTP 客户端。如果你在意可控性向量化可以先从本地的小模型或者简单的 TF-IDF 起步不一定要上远程 embedding 接口工具数量少的时候效果差距没那么大。目录结构我习惯这样组织agent_reach/ registry.py # 工具注册表 router.py # 路由识别节点 executor.py # 执行器 memory.py # 记忆与上下文 guard.py # 权限与安全 trace.py # 观测埋点 tools/ # 各工具的具体实现把工具实现单独放一个目录的好处是注册逻辑和业务逻辑分离改工具不用碰框架代码。这个习惯在项目做大之后能救命。5.2 执行循环主流程下面是主循环的骨架省略了具体业务def run_agent(user_input: str, registry, router, executor, memory, guard, max_steps: int 8): memory.append_user(user_input) for step in range(max_steps): ctx memory.build_context() candidates router.recall(user_input) decision decide_with_llm(ctx, registry, candidates) if decision.is_final: memory.append_assistant(decision.content) return decision.content tool registry.get(decision.tool_name) if tool is None: memory.append_system(f工具不存在: {decision.tool_name}) continue if not guard.allow(tool, decision.args): memory.append_system(该操作需要人工确认已暂停) return 需要你确认后再继续 args validate_args(tool, decision.args) result executor.call(tool, args) memory.append_tool(tool.name, result) return 达到最大步数已停止这里有几个参数值得展开。max_steps我一般设 8 到 12太小会让复杂任务提前中断太大则可能陷入无效循环并推高成本。实测下来 10 是个比较平衡的值超过这个步数还没收敛通常说明任务本身有问题继续跑也是浪费。validate_args这一步千万别省。它的逻辑是按契约检查必填项、校验枚举、做类型转换、补默认值。失败时不要把异常抛给用户而是把错误信息作为系统消息喂回模型让它重新生成参数。这个反馈回路是触达层自我修复的关键。5.3 记忆与上下文压缩agent 记忆是另一个容易翻车的点。工具调用产生的原始返回往往很长一个接口返回几千字 JSON 很正常。如果每次都全量塞进上下文几轮之后上下文就爆了模型还会因为信息过载而抓不住重点。我的压缩策略分三层。第一层是结构裁剪只保留返回里跟当前任务相关的字段其余丢掉。第二层是摘要替换调用完成后立刻生成一句简短摘要原始结果存到外部只在上下文里留摘要加一个引用 ID。第三层是窗口滑动超过一定轮数后把最早的几轮压缩成一段整体摘要。注意压缩时最容易丢的是失败的细节。很多人只保留成功结果把失败原因压缩掉结果模型反复尝试同样的错误路径。失败信息哪怕只有一行也要完整保留。5.4 接入浏览器与外部系统的注意事项agent browser 类工具是触达层里最脆弱的部分因为外部页面会变、会加载慢、会有弹窗。我在接入这类工具时定了几条硬规矩每次操作后必须显式等待目标元素出现不允许用固定 sleep 硬等。所有选择器都要有备用方案主选择器失效时能自动降级。单步操作超时时间设短一点五分钟那么长的超时纯属浪费宁可失败重试。截图和页面文本在关键步骤都留一份排查问题时这是唯一的现场证据。另外像 pi agent 桌面端、orca agent 这类以桌面形态交付的 agent 运行时它们在触达本机系统时确实有优势但权限边界也更危险。如果你的工具集里包含文件写入、进程启动这类能力桌面端的权限配置一定要比服务端更保守。6. 安全与权限触达层最容易出事的地方安全这块我在项目早期是完全忽略的因为测试环境无所谓。直到有一次在演示时agent 因为理解偏差把一个清理测试数据的指令执行成了真实删除操作我才意识到权限设计不是可选项。agent 安全这个话题最近讨论得很多但大部分内容停留在概念层面落地的具体做法讲得少。6.1 权限分级设计我的分级很简单就三档多了反而没人认真执行等级覆盖操作处理方式read查询、读取、搜索直接执行记日志write创建、更新、发送执行前记录完整参数支持事后审计dangerous删除、批量修改、资金相关、外部通知强制人工确认默认拒绝关键在第三档的默认拒绝。也就是说即使模型明确要求执行危险操作系统在没有人工确认的情况下也不会执行。这个默认值设计救过我至少两次。分级信息就存在注册表的risk字段里执行器在调用前先查这一项再交给 guard 模块判断。整个链路上只有一处判断逻辑改起来不容易漏。6.2 危险操作拦截与人工确认人工确认的交互设计有讲究。不要把整个工具调用原封不动丢给人看那样人看不懂。应该转换成人类可读的描述要做什么、影响哪些数据、可不可以撤销。我见过把原始 JSON 弹给用户确认的实现用户根本不知道该点同意还是拒绝最后所有人都闭着眼睛点同意确认环节形同虚设。确认信息里必须包含不可撤销这类明确提示。如果是幂等操作也要说明重复执行不影响结果让人能安心判断。6.3 沙箱与凭证管理凭证管理的原则是模型永远不接触真实凭证。工具执行时由执行器在服务端注入凭证模型只知道工具有什么能力不知道背后用什么密钥。这个隔离做起来不难但很多项目图省事直接把凭证写进工具描述里一旦提示词被泄露就是安全事故。沙箱方面能隔离就隔离。文件操作限定在指定目录网络请求限定在允许的域名列表命令行执行限定在白名单命令。这几条限制会牺牲一点灵活性但换来的是出事时的可控范围。7. 可观测、回滚与错误排查触达层出问题时如果没有足够的现场记录排查会变成纯粹的猜谜。我现在的习惯是任何一次工具调用无论成功失败都落一条结构化记录。7.1 一次执行链路的埋点设计每条记录至少包含这些字段会话 ID、步骤序号、工具名、参数摘要敏感字段脱敏、开始时间、耗时、结果状态、错误类型、重试次数、选择的候选集和置信度。最后两项经常被忽略但排查误选问题的时候它们至关重要。没有候选集记录你根本不知道当时路由节点考虑了哪些选项也就无从判断是召回出错还是终选出错。7.2 常见报错与对症处理下面这张表是我从真实项目里整理出来的左列是现象右列是排查方向现象大概率原因处理方式agent couldnt generate a response上下文超限或工具描述过长检查 token 用量压缩记忆agent execution terminated due to error工具抛异常未捕获在执行器加统一异常兜底反复调用同一个工具结果没被正确写回记忆检查结果归一化逻辑参数总是缺字段契约描述不清或模型未收到完整 schema补全必填项说明并回喂错误安装中途回滚失败依赖版本冲突或磁盘权限保留日志按依赖树逐层清理调用超时但无日志超时未传到执行器统一超时配置入口第三行那个问题我遇到过好几次本质是记忆写入和结果归一化之间的时序问题。工具返回了但归一化步骤抛了异常结果没写进上下文模型以为没执行过就又调了一遍。修法很简单把归一化放进 try 里异常时也要写一条失败记录进上下文。7.3 回滚与幂等回滚的前提是幂等。如果工具本身不幂等回滚就没有意义因为重复执行会叠加副作用。所以我在注册表里留了idempotent字段非幂等的工具在重试逻辑上必须特殊处理要么不重试要么重试前先做状态检查。对于写操作我更倾向的做法是补偿而不是回滚记录下正向操作出问题时执行一个语义上相反的操作。补偿逻辑需要每个工具单独设计没法通用化这部分工作量经常被低估。8. 测试与评测怎么证明触达层可靠agent 测试跟传统软件测试最大的区别是输出不完全确定。但这不代表不能测。我的做法是把测试分成两层确定性的触达层测试和概率性的端到端测试。前者追求全覆盖后者追求统计意义上的稳定。8.1 四类测试用例正常路径给定合法输入工具被正确选中、参数正确、结果正确回收。这类用例应该占多数也是回归测试的主体。边界路径空参数、超长参数、特殊字符、极值数值。这类用例往往能暴露出参数校验的漏洞。恶意路径注入类输入、越权尝试、诱导执行危险操作。这类用例不用多但必须有而且每次改动权限逻辑后都要重跑。故障路径模拟超时、模拟返回格式异常、模拟下游不可用。验证兜底逻辑是否真的生效。8.2 指标与回归集端到端层面我关注四个指标工具选择准确率、参数一次通过率、任务完成率、平均步数。前两个是触达层的直接指标后两个反映整体效果。回归集的建设方式是把线上真实失败案例脱敏后入集每次修复一个问题就往里加一条。这个集合会越滚越大但它的价值也越滚越高。我的经验是回归集超过两百条之后它就成了整个项目里最有价值的资产比任何文档都管用。提示回归测试要定期全量跑但日常开发用小样本快跑。我的划分是每次提交跑 20 条核心用例每天定时全量跑一次兼顾速度和覆盖。9. 多 Agent 协作与 A2A 接入单 agent 的触达做扎实之后很自然会遇到多 agent 协作的需求。这里我想先泼一盆冷水大多数场景不需要多 agent。把一个 agent 加几个工具能解决的事拆成三个 agent 互相通信复杂度是翻倍的收益却经常是负的。9.1 什么时候才真的需要多 Agent我的判断标准有三条满足其中两条才考虑拆分任务领域差异极大一个提示词里塞不下需要在隔离的权限边界内运行比如安全审查必须独立子任务的执行时间差异巨大需要并行。拆分之后触达层的设计要变。单 agent 时工具调用是内部行为多 agent 时变成了跨边界通信。这时候协议就重要了。9.2 Agent Card 与协议版本差异的坑A2A 这类协作协议里agent card 是描述一个 agent 能力边界的元数据。它的作用类似于工具注册表只不过描述的对象从工具变成了 agent。这里有个很实际的坑协议版本之间的字段差异。1.0 版本和 0.3 版本在 agent card 的字段定义上并不完全兼容有些字段换了名字有些从必填变成了可选。我在对接时就因为版本不一致导致对方的能力描述解析失败表现是这个 agent 声称能做但实际调不通。排查时第一件事就是核对双方的协议版本声明别急着改代码。另一个坑是能力声明的边界。agent card 里声明的能力往往比实际实现的范围大因为声明容易实现难。接入别人的 agent 时一定要做一次实际调用验证不能只看声明。10. 常见问题速查与学习路线10.1 高频问题速查表整理一份我经常被问到的清单都是实打实的坑工具越多越好吗不是。工具超过二十个之后路由准确率会明显下滑。该合并的合并该按场景分组的按场景加载。一定要用向量召回吗不。工具少的时候直选足够向量召回是给规模准备的。记忆该存多久按会话存跨会话的长期记忆慎重开放容易引入污染。错误重试几次合适幂等操作两次非幂等一次都不重试。怎么防止 agent 跑飞最大步数、单步超时、危险操作确认三个都要有。学习路线怎么安排先跑通单工具调用再做路由再做记忆最后做权限和观测。10.2 一条能走通的学习路线如果你的目标是系统掌握 agent 开发我建议这个顺序第一步写一个只有两个工具的最小循环理解模型怎么决定调用第二步加路由体会工具变多之后的准确率变化第三步加记忆和压缩感受上下文管理的重要性第四步加权限和观测这部分是区分玩具和工程的分水岭第五步做评测集让系统能持续改进。每一步都要跑通再往下走不要跳。我见过太多人一上来就搭多 agent 框架代码写得挺花哨一问工具误选率多少就答不上来。基础不牢后面的东西都是空中楼阁。关于学习资料上海交大那套 agent 教程在网上流传比较广入门阶段可以参考但别只看教程不动手。具身智能 agent 那类方向跟软件 agent 的触达层思路相通但细节差异很大感兴趣可以了解但不要一开始就扎进去。最后分享一个我在实际项目里的体会。触达层这东西做好了没人夸因为它表现正常的时候是隐形的一旦没做好所有问题都会归咎于agent 太笨。我花了很长时间才接受这个现实把力气花在那些没人看见的地方是 agent 工程化的必经之路。注册表里每一条认真的工具描述、执行器里每一次老实的超时控制、日志里每一条完整的现场记录这些不起眼的细节累加起来才是一个 agent 从演示能跑到生产能跑的全部差别。