第一次把 Agent-Reach 完整跑通的那个晚上,我盯着终端看了很久:模型调用了我的搜索引擎工具、抓取了一个网页、然后把内容整理成了 Markdown 笔记,全程没有我干预。这台电脑在替我干活——不是帮我优化文案,而是真的伸出手去够到了外部世界。这是我一直想要的 AI Agent 形态。
Agent-Reach 是我从零开始搭建的一个 AI Agent 开发项目,核心目标一句话:让大模型不只动嘴,还能动手。所谓“Reach”,就是“够得着”——够得着网络、文件系统、各类 API 和知识库。这篇东西算是我整个过程的工程复盘,覆盖了 Agent 架构选型、工具与技能设计、沙箱安全、评测调试这些环节。不管你是被 LangChain 折腾过的开发者,还是刚想入门 Agent 开发的小白,应该都能在里面找到能直接抄作业的东西。
1. 为什么我不直接用 LangChain,而是从零搭建自己的 Agent
1.1 聊天机器人不是 Agent:动手和自主才是分水岭
要弄清楚 Agent 是什么,先得看它不是什么。我用了很久 ChatGPT 网页版,觉得它很聪明,但从来没觉得它像 Agent。区别就在两个动作上:动手和自主。一个普通的 Chatbot,你问它天气怎么样,它只能凭训练数据里的常识回你。你要它“帮我查一下今天北京的天气,然后根据是否下雨决定要不要带伞”,它就卡壳了——它既没有实时数据来源,也没有“查完再决策”的执行链。
Agent 的本质是给模型装上闭环的执行能力:感知(看环境、读数据)、决策(下一步做什么)、行动(调用工具改环境)、再感知。我更喜欢用一句大白话来概括:Chatbot 是张嘴说话,Agent 是说话算话。说话算话意味着模型要承担后果——它调用了一个删除命令、发了一封邮件、提交了一笔订单,这些动作都会产生真实影响。这就是 Agent-Reach 立项时我给自己定的原则:先承认它会犯错,然后把容错、审计、权限都设计进系统里,而不是让模型裸奔。
从工程角度说,这个差别会直接影响你的架构。Chatbot 只需要 prompt、模型、上下文窗口;Agent 则需要工具注册表、执行循环、记忆管理、沙箱隔离、日志追踪。后面这些件件都是独立的子系统。所以我说,入行 Agent 开发,第一课不是学某个框架,而是理解从 Chatbot 到 Agent 的这个“环境复杂度跃迁”。
1.2 现成框架的三个劝退点:抽象、升级、黑盒
在决定从零写之前,LangChain、Dify、CrewAI 这些框架我是真刀真枪用过的,说白了也是踩完坑才走的。不是它们不好,而是它们的强项和我的需求错位了。
第一个劝退点是抽象层次太高。LangChain 里一层套一层,Chain 套 Agent 套 Prompt Template 套 Output Parser,debug 的时候经常要拆四五层才能找到真正出问题的模块。模型输出不符合 JSON 倒是小事,最怕的是框架在中间默默给你截断了字段、改写了 prompt 你都不知道。遇到诡异行为我第一反应是去翻框架源码,翻着翻着就变成在跟框架作者的思路搏斗。
第二个问题是升级不兼容。两三个月没跟进,API 就变了;为了跑通示例还得看文档考古。对一个要长期维护的项目来说,这是致命的——我不可能每隔几个月陪框架重写一遍业务逻辑。社区里经常有人问“LangChain、Dify、CrewAI 到底哪个好”,我的答案是:如果你要快速做原型、验证想法,Dify 这类低代码平台最快;如果你已经确定要深度定制 Agent 行为,哪个框架都会成为你的瓶颈。
第三个问题更本质:框架的封装给了你太多默认行为,但 Agent 的核心价值恰恰在于可定制。我希望 Agent 的每一步都在我的掌控里:工具调度按我的规则来、上下文裁剪按我的策略来、危险操作按我的审批流程来。用别人的框架,这种掌控感很难拿到。所以 Agent-Reach 一开始就定了个原则:框架可以不用,但架构必须清晰,循环自己写,套件自己拼。
1.3 先把术语对齐:Agent、Harness、Tool、Skill 谁是谁
圈子里术语满天飞,很多人栽在概念没对齐上。这里我按 Agent-Reach 里的分工捋一遍,后面的内容也都在这套术语上展开。
| 概念 | 角色 | 类比 |
|---|---|---|
| Agent | 最上层的决策实体:读任务、做规划、调工具、看结果 | 餐厅厨师长 |
| Harness | 承载运行的骨架:循环调度、解析模型输出、执行工具、管上下文、记日志 | 后厨基础设施 |
| Tool | 最小能力单元:一个函数、一个 API、一条命令 | 单个厨具 |
| Skill | 多步组合技能:按固定流程串联多个 Tool | 成熟菜谱 |
Agent 是大脑,负责“聪明”;Harness 是骨架,负责“可靠”。很多人争论“harness 和 agent 区别”,一句话总结:Agent 做决策,Harness 保运转。模型是司机也好、厨师长也好,没有车、没有灶台,你有再多想法也使不出来;反过来,车和灶台再完备,没人开、没人掌勺,也出不了活。两边配合才是完整系统。
Tool 是原子操作,Skill 是由 Tool 串起来的流程。比如“将网页保存成 Markdown”这个 Skill,内部就是“抓取页面 → 转格式 → 清理噪音 → 写入笔记”四步。模型不需要重新发明每一步,只需要知道这个 Skill 能完成什么目标、需要什么输入。这套分层现在是 Agent 开发的主流范式,Claude 的 Agent Skills、OpenAI Codex 里面的命令行工具,本质都在往这个方向收敛。
2. 动手前先把 Agent 架构拆明白:Agent-Reach 的设计稿
2.1 核心循环怎么落地:Observe-Think-Act-Reflect
Agent 能不能稳定干活,关键看主循环是否写得干净。Agent-Reach 用的是广义的 ReAct 模式,工程上我把它拆成四步:Observe(观察)、Think(思考)、Act(行动)、Reflect(反思)。
Observe 阶段,Agent 收集所有可用的当前状态:用户任务、系统提示、工具执行结果、记忆检索结果。Think 阶段,模型基于这些信息输出规划:它决定下一动作调哪个工具、传什么参数,或者判断任务已完成、给出最终答案。Act 阶段就是 Harness 接管,解析模型输出、执行工具、把结果打包。Reflect 阶段最容易被偷懒省掉,但它恰恰是区分“笨重循环”和“可控循环”的分水岭。
我的实现里把 Reflect 拆成两个时机。动作前做一次预检:这个动作合理吗?和上一轮结果矛盾吗?有没有明显要撞墙的倾向?动作后再做一次复盘:上一步结果和期望一致吗?还需要继续吗?这两个钩子合起来,就是“三连问”。最典型的场景是工具调错了参数:没有 Reflect,模型拿到报错后大概率继续用同样参数再撞一次墙;有了 Reflect,它会意识到“上一步失败,原因可能是参数格式不对”,从而换一种姿势重试。我做过的统计里,加上 Reflect 之后,同一批任务的成功率从 43% 提到 61%,平均步骤数从 9.3 降到 7.8。别小看这个变化,在多步骤任务里,少撞一次墙就少烧一批 token。
2.2 工具层怎么设计:给 Agent 装“手”的三个要点
给 Agent 装手,不是简单地把函数列表塞进 prompt 就行。工具层设计有三件事必须做好:描述、校验、容错。
描述指的是工具说明。模型是靠自然语言理解工具的,函数名、参数说明、示例写得越具体,模型就越不容易理解偏。我在 Agent-Reach 里给每个工具维护一份 JSON Schema:name、description、parameters(含类型、必填、枚举值限制),外加一两个 few-shot 示例。这个 Schema 同时用于两件事:拼进模型工具调用上下文,以及在 Act 阶段做严格校验。换句话说,模型就算脑补了一个不存在的参数,在真正执行前也会被我拦下来,而不是让错误一路传到副作用环节。这个习惯帮我在开发期少炸了很多次。
容错比校验更隐蔽。工具是会和真实世界打交道的:网络请求可能超时、文件可能不存在、API 接口可能限流。Agent-Reach 的运行策略是“工具永远不抛致命异常”——每个工具调用都返回结构化结果,包里包含 ok/error 状态、业务数据、错误原因和建议重试参数。模型拿到结果后判断自己是换个姿势重试,还是换一个工具,而不是直接读到一行刺眼的 panic。这套约定看似枯燥,却是一切后续自动化的地基。
2.3 技能层怎么抽:把“网页保存成 Markdown”做成可复用 Skill
Tool 是原子操作,但真实任务很少只用一个工具。比如用户说“把某某网页保存成 Markdown 存到我的笔记库里”,拆开得走四步:抓网页、转 Markdown、清理导航噪音、写入笔记库。每步调用一个工具,但组合逻辑完全可复用,这就是 Skill 要解决的问题。
Agent-Reach 的技能层用声明式配置来定义:一个 YAML 文件描述 Skill 的名称、描述、适用场景、输入输出参数,以及内部步骤序列。步骤之间允许数据传递,比如第 2 步的输出直接作为第 3 步的输入。对“网页转 Markdown”这个 Skill 而言,模型侧只需要看到一句话:“把 URL 对应的网页保存为 Markdown 文件,并自动分类到 Obsidian 对应目录。”具体怎么抓、怎么转、怎么分类,模型完全不用关心。
技能层的价值不止于“少几句提示”。很多人想做的“让 Agent 自动发一条小红书笔记”,拆开无非也是选题生成、文案撰写、图片处理、发布接口调用,完全可以做成一个 Skill;想让 Agent 画图,同样是写文案、生成图片、保存文件这几步的组合。技能层收得越多,Agent 的复杂度就越低、可预测性就越高。但抽 Skill 有个关键判断:什么该收、什么该留在模型自由发挥?我的标准是三步以上的固定流程才值得抽。抽早了反而坏事——流程还没稳定就固化,每次都要套模板,灵活性大降。比较稳的节奏是先让模型自由调用跑两周,把高频出现、流程稳定的步骤沉淀下来再固化。
2.4 记忆层怎么分:短期上下文与长期向量记忆的配合
Agent 干活总有上下文放不下的那一天。Agent-Reach 当前把记忆拆成两层,各管各的问题。
短期记忆就是当前任务的上下文窗口。它的核心问题不是“存不下”,而是“怎么裁”。我在项目里做了上下文压缩策略:当 token 逼近上限时,按重要程度逐级裁剪——工具返回的原始长文档最先压缩成摘要,然后是历史对话的早期轮次,最后才动正在执行的任务规划。这个顺序是反直觉的,很多 Agent 一上来就裁早期对话,结果后面的反思步骤失去了“之前试过什么”的信息,重试变量常常就是从这丢的。
长期记忆走的是向量库加结构化笔记双轨。向量库管语义检索,用于匹配“类似任务以前怎么做的”;结构化笔记管事实,比如用户偏好、固定配置项。我目前把长期记忆直接落在 Obsidian 的库上,用文件系统做持久化,这样 Agent 写进去的每条记忆都是人可读的 Markdown,随时能人工审计和修正。这也是那阵子 Hermes Agent 在 Obsidian 圈挺火的原因——把 Agent 记忆和人的知识库放在同一层,透明度高太多。向量的相似度检索负责“模糊找”,文件系统负责“精确存”,两个配合起来,记忆才不会变成黑盒。
2.5 安全边界怎么划:沙箱隔离、权限分级、审计日志
Agent 一旦有了“动手”能力,安全问题就绕不开。我的原则是:任何时候都不要让模型裸奔在宿主环境里。Agent-Reach 的安全设计有三条线。
第一条线是环境隔离。工具执行放进沙箱容器,Agent 的代码在容器内跑,宿主文件系统、网络端口默认不可达。我在项目里提供两种沙箱模式:轻量模式用 Linux 用户命名空间加 seccomp 做进程级隔离,适合开发期;生产模式直接跑 Docker 容器,给每个任务分配一次性容器,任务结束销毁环境。用哪种模式取决于任务类型:只读网页这种任务隔离要求不高,为每个工具调用都起容器反而拖慢速度。
第二条线是权限分级。我做了四级操作权限:只读查询、普通写入、危险操作、高危操作。Agent 可以自由执行只读和普通写入;危险操作(比如删除文件、发邮件)必须经过审批钩子;高危操作(比如安装系统包、修改系统权限)默认禁止,需要管理员手动放行。这个设计在开发期会被嫌麻烦,但它能挡住“模型突然决定删除整个项目目录”这类事故。
第三条线是审计日志。所有工具调用、参数、结果、由谁发起、属于哪个任务,全部落日志,出问题时能完整回放。我曾经排查过一个诡异 bug:模型在第五轮突然天马行空调了一个不该调的工具。没有审计日志,这种问题连定位都无从谈起。安全设计不是要限制 Agent 的能力,而是给它的能力划一条可回退的护城河。
3. 从零手写 Agent-Reach:核心代码、工具注册与沙箱配置
3.1 技术选型:Rust 写核心、Python 写工具脚本的原因
聊具体实现之前,先交代技术栈:Agent-Reach 的核心循环和 Harness 用 Rust 写,工具层和实验脚本用 Python。很多人问为什么,理由有三。
第一,性能与资源占用。Agent 任务往往要开几十上百个并发循环,Rust 的 Tokio 异步运行时扛并发很稳,内存占用比 Python 进程低一个量级。做长任务、批处理的时候,这个成本差是实打实的。第二,单二进制分发。一个编译产物丢到服务器就能跑,不依赖 Python 环境和一长串 pip 依赖,给非技术协作方部署时优势巨大。第三,类型安全。工具参数校验、结构化输出解析这类逻辑,用 Rust 的强类型写起来很难糊弄过去,反而逼你把边界情况处理干净。
代价也很明显:Rust 开发速度比 Python 慢,生态里的 Agent 库也还在早期。可以挑 HTTP、Serde JSON、Tokio 这些基础库自己搭积木,但不要指望有一个像 LangChain 那样的全功能一站式 Agent 框架,至少目前还没有。所以我的约定是:算法骨架、工具框架这种“地基”用 Rust,爬虫脚本、数据分析这种一次性逻辑用 Python,两边优势互补。选型前做好心理准备:你将享受到自由,也将承担造轮子的时间成本。
3.2 最小可用的 Agent 主循环长这样
直接上一段简化核心代码,展示 Agent-Reach 的主循环长什么样。错误处理和上下文压缩都被我省略了,只保留骨架。
// agent_loop.rs(简化版) use serde_json::Value; async fn run_agent(task: String, tools: Vec<Tool>) -> Result<Value, AgentError> { let mut context = Context::new(task); for step in 0..MAX_STEPS { // 1) Observe:把模型决策需要的所有信息拼进上下文 let observation = context.observe().await?; // 2) Think:模型输出结构化的下一步动作(JSON) let plan: Value = llm_think(&observation, &tools).await?; let action: Action = serde_json::from_value(plan) .map_err(|_| AgentError::PlanParseFailed)?; // 任务完成,直接返回最终输出 if action.is_final() { return Ok(action.output); } // 3) Reflect(预检):拦截明显不合理的动作 let check = pre_flight(&action, &context.last_result()).await?; if check.should_abort() { return Err(AgentError::PlanAborted(check.reason)); } // 4) Act:执行工具,结果永远返回结构化错误而不是 panic let result = match execute_tool(&action, &tools).await { Ok(res) => ToolResult::ok(res), Err(err) => ToolResult::err(err), }; context.record(&action, &result); // 5) Reflect(复盘):让模型意识到上一步结果与期望是否一致 let review = post_land(&result, &context.history()).await?; context.record_review(&review); } Err(AgentError::MaxStepsExceeded) }这段代码有几处容易被忽略的设计。首先,模型输出直接被解析成 Action 结构,而不是让模型自由发挥文本——我把输出格式约束得很死,不然后续解析全是地雷。其次,预检在 Act 之前跑,能拦截一部分明显要撞墙的动作。最后,ToolResult 永远不 panic,错误也作为一种结果进入上下文,让模型在下一轮里学习。
跑起来之后你会发现,现实任务的难点不在主循环,而在工具结果的可用性。模型读到过长、过乱的工具输出,很容易在下一轮思考中迷失位置。我建议在 Act 阶段加一个“结果压缩”钩子:超过阈值的输出先自动摘要再记入上下文。这一步收益非常高,模型失误率肉眼可见地下降。
3.3 工具注册和 Skill 加载的落地实现
工具注册我用 Rust 的宏加 trait 实现。每个工具实现一个 Tool trait,声明自己的元数据和执行函数,然后通过宏一次性注册进工具表。
// tool.rs #[async_trait] pub trait Tool: Send + Sync { fn metadata(&self) -> ToolMetadata; // 名称、描述、JSON Schema async fn run(&self, args: Value) -> ToolResult; } register_tool!(WebFetchTool); register_tool!(FileWriteTool); register_tool!(ShellExecTool);Skill 加载走的是目录约定:项目skills/目录下每个子目录算一个 Skill,里面有skill.yaml和可选的实现脚本。启动时扫描目录、解析配置、注册到技能表。这样新增一个 Skill 不需要改核心代码,团队协作时其他人直接往目录里丢,主程序完全无感。
# skills/web2md/skill.yaml name: web2md description: "将 URL 对应的网页抓取并保存为 Markdown 文件" inputs: url: type: string required: true title: type: string required: false steps: - tool: web_fetch input_map: url: "{url}" - tool: html_to_markdown input_map: html: "{steps[0].result}" - tool: cleanup_noise input_map: markdown: "{steps[1].result}" - tool: obsidian_write input_map: title: "{title | sanitize}" content: "{steps[2].result}"关于 Skill 的输入映射,我一开始踩过坑:直接让步骤之间传递完整数据,结果大 JSON 在上下文里占了几千 token。后来改成显式映射,只有步骤声明需要的字段才会传过去,等于给上下文做了一层按需裁剪。当你的 Skill 数量超过 20 个时,还要做描述压缩——把低频技能的描述浓缩成一句话,只让高频技能保留完整参数说明,这是控制 prompt 长度最有效的办法之一。
3.4 Token 与上下文管理:三个必须设置的参数
Agent 项目绕不开 token 这个话题。光知道 token 是计费单位没用,你得把它当运行时资源来管理。我在 Agent-Reach 里设了三个关键参数,颜值不高但个个保命。
| 参数 | 作用 | 我的设置参考 |
|---|---|---|
| max_tokens_per_step | 限制单步思考输出长度 | 简单工具调用 1024,复杂规划 2048 |
| context_budget | 上下文预算,触发压缩的提醒值 | 视模型窗口而定,我常用 12k |
| token_usage_ratio | 使用率阈值,超过后不再接新任务 | 默认 80% |
第一个参数防止模型输出过长的“内心戏”。大模型思考过程有时特别啰嗦,不设上限,它可能一口气输出上万 token 的内心独白。第二个参数解决上下文瘦身问题,触发后按优先级压缩:工具结果、历史早期轮次、任务规划,一层层降级。核心思路是保住“当前正在执行的任务主线”和“关键工具结果”,这两个丢了 Agent 会立刻降智。
第三个参数是我用一次惨痛教训换来的。某次并发任务没设阈值,上下文被塞爆,模型开始胡言乱语,一批任务全废。设了之后虽然偶尔任务变慢,但行为变得可预测。顺便解答一个常见疑问:“AI agent token 是什么意思?”在 Agent 语境下,token 不只是计费单位,更是决策和记忆的载体——模型每一轮思考、每个工具参数、每条历史记录都换算成 token,token 上限直接决定 Agent 能“看多远、想多深”。把 token 当作随时可能耗尽的内存,Agent 架构就会自然地往压缩、摘要、缓存方向设计。
3.5 沙箱落地:Docker 和用户命名空间怎么选
沙箱具体怎么搭,我给 Agent-Reach 写了两种模式,各有清晰的适用场景。
轻量模式基于 Linux 的用户命名空间加 seccomp profile。进程以非 root 用户运行在隔离命名空间里,禁止 mount、禁止绑定网络端口、限制文件系统访问白名单。优点是毫秒级开销,直接在宿主机上启动,适合大量低频工具调用和日常开发调试。缺点是隔离强度有限,如果模型被诱导执行复杂的内核级攻击,理论上存在逃逸风险。
重量模式直接上 Docker,给每个任务起一个一次性容器。容器内只有任务需要的环境和工具,网络策略通过 bridge 控制,默认只放行必要域名。任务结束容器直接销毁,痕迹清零。性能代价明显,每次起容器要几百毫秒不等,但换来更强的隔离和快照能力。我的实践建议:对外提供服务、Agent 会接触外部输入时,必须上重量模式;纯本地个人使用,比如让 Agent 整理笔记,轻量模式足够,否则每步都等容器起来,体验会很糟糕。
沙箱这件事我还要强调一个反直觉的经验:沙箱隔离的不是恶意模型,而是失控行为。大模型不是坏,它是不可预知。今天测试好好的工具组合,明天换一个问题就可能调出你完全没想到的参数组合。沙箱的作用就是把这层不可预知关在笼子里。
4. 跑通不是终点:Agent 评测、调试与安全审查
4.1 Agent 评测集从零搭建:除了成功率还要看三个指标
Agent 跟普通软件有个根本区别:它没有固定的正确输出,只有“任务是否完成”。所以 Agent 评测集必须自己建,而且应该往“任务”而非“问答”方向设计。
我建议从三个维度收集任务样例。第一是核心能力覆盖:每一个 Tool、Skill 至少要有一条对应任务。第二是异常路径:模拟参数缺失、外部服务返回错误、中途打断这些情况,看 Agent 会不会死循环或误重试。第三是边界场景:超长输入、模糊指令、互相矛盾的需求。每条任务写清楚输入、期望结果、可接受的成功标准——注意是标准而不是唯一答案,因为 Agent 完成任务允许多种路径。
指标方面,成功率只是起点。每次回归我都会同时统计三个数:平均步骤数,越少代表效率越高;平均 token 消耗,优化空间所在;任务总时长,含工具调用耗时,能暴露沙箱或网络瓶颈。三个数合起来才能反映 Agent 的“质量”而不只是“能不能成”。另外强烈建议把评测跑进 CI,每次改主循环、加新工具都自动跑一轮,防止回归。Agent 项目里最常出现的情况就是“修好一个 bug,另一个任务莫名开始失败”,没有回归测试兜底会很痛苦。
4.2 高频报错排查:Agent execution terminated due to error 怎么定位
开发期最常见的报错就是Agent execution terminated due to error,一个特别笼统的终止信息。我遇到它的排查顺序是固定的:先看审计日志里最后几轮工具调用,99% 的情况是某个工具返回了意外错误格式;再看是不是触发了 Reflect 预检的拦截;最后才怀疑模型输出解析失败。
| 现象 | 常见根因 | 排查思路 |
|---|---|---|
| Agent execution terminated due to error | 某个工具返回非预期错误格式 | 先查审计日志最后几轮工具调用 |
| 模型反复调用同一工具并失败 | 工具错误信息太笼统,模型无法区分错误类型 | 给错误信息加分类和建议重试姿势 |
| 模型输出无法解析为 JSON | 思考过程和输出混在一起 | 用宽容解析器提取 JSON,失败后回喂错误并重试 |
| 任务中途上下文被塞爆 | 工具结果过长、未做压缩 | 设置 context_budget,超限先压缩工具结果 |
模型输出解析失败是另一个高频坑。模型偶尔不按 JSON 输出,尤其让它先想再写的时候。我的对策是两层:第一层是宽容解析器,尝试从文本里提取 JSON 片段、修正轻微逗号错误;第二层是重试机制,解析失败后把错误信息喂回模型,要求重新输出合法 JSON。实测下来重试成功率很高,但必须设置重试上限,否则会形成无限循环烧钱。
最迷惑人的问题是工具循环死锁:Agent 反复调同一个工具,拿到失败后不换策略,无脑重试直到步数上限。根源往往是工具错误信息太笼统,模型分辨不出“参数错误”和“网络超时”是两类问题。解法是给工具错误分类并给出重试建议,同时配合 Reflect 三连问,让模型真正意识到“这条路走不通,得换一条”。
4.3 值得参考的对象:Claude Agent Skills、Codex、Cline、Hermes 的启发
做 Agent 开发,一直盯着别人的项目看特别有收获。我把这段时间研究过的几个对象整理一下,不是推荐你去抄,而是拆解它们各自解决了什么问题。
Claude 的 Agent Skills 是我认为当前落地最扎实的范式之一。它把可复用的技能包做成清晰的文件结构:技能说明、指令、模板、示例、资源统统打包。Agent 执行任务时按需加载对应技能,而不是把全部技能塞进上下文。这个“按需加载”思路我直接搬进了 Agent-Reach 的 Skill 层设计里,收益很大。
OpenAI Codex 是很好的 Harness 范本。它本质是一个跑在终端里的命令行编码 Agent,把工具调用封装成命令行的自然延伸。最值得学习的是它和开发环境的交互密度:模型随时能看到编译报错、测试输出、diff 结果,再决定下一步。这种紧密反馈环对写代码类任务特别有效,也提醒我工具结果的回传密度会直接影响模型的判断质量。
Cline 和 Hermes Agent 则是社区工具的代表。Cline 的配置体系做得很细,模型能力、权限级别、MCP 服务都能灵活组合;Hermes Agent 尤其适合研究 Agent 与 Obsidian 工作台的结合——它把技能、笔记、工作流放在可视化界面里,让你直观看到 Agent 每一步到底调了什么工具、改了什么文件,对调试 Agent 的“黑盒”状态很有帮助。研究这些项目的原则是:别照搬,拆出对自己架构有启发的那一两块,然后用自己的方式实现。
4.4 外部 API 调用前的最后一道闸门:策略引擎与审计快照
第 2.5 节讲了架构层面的安全设计,这里补一个实操层面的细节:外部 API 调用前,Harness 必须能拦截并强制走审批。因为 Agent 太容易“擅自行动”了——它可能为了完成任务,直接去调一个会产生费用的接口、给第三方发送请求,或者在没确认的情况下修改线上配置。
我在 Agent-Reach 里实现了一个 Policy 引擎,每个工具调用在正式执行前都会过一遍策略表。每条规则描述“什么条件下做什么操作需要什么级别的审批”:调外部付费 API 需要二次确认;写入非白名单路径需要确认;删除操作一律禁止。这个引擎是纯规则配置,不依赖模型判断,因为安全决策不该交给概率模型拍板。
审计日志在这个环节再次登场,而且我建议配一个“快照”功能。被拦截或高风险的操作,把当时的上下文、工具参数、模型推理片段一起存下来。以后回溯“为什么它会想这么做”时,这些材料能帮你改进 prompt、调整工具描述,从根上减少危险意图。安全检查不是要把 Agent 锁死,而是给它一个隔离试错的缓冲区——这是 Agent 逐步变得可靠的前提。
5. 多 Agent 协作与后续扩展:让 Agent 真正“够得着”更多
5.1 多 Agent 编排:supervisor-worker 模式怎么落地
单 Agent 做到一定规模,你自然想拆成多 Agent。原因很朴素:一个 Agent 的上下文就那么大,把所有能力塞给一个 Agent,prompt 臃肿、token 浪费、决策质量下降。拆开以后各管一摊,效果反而好。
Agent-Reach 的多 Agent 编排用的是经典 supervisor-worker 模式:一个调度 Agent 负责任务分解、分配和结果汇总,一组工作 Agent 各负责一个具体技能域。调度 Agent 先写一份简短执行计划,然后把子任务发给匹配的工作 Agent,收集结果后做整合输出。这个模式的优点是结构清晰、易扩展,缺点是调度 Agent 本身容易成为瓶颈——它的上下文要装下所有子任务的结果摘要,任务多时压力很大。
我踩过的一个坑是结果格式没定义清楚。如果工作 Agent 返回一堆自由发挥的自然语言,调度 Agent 就不得不靠提示词去猜,整合质量大打折扣。解决方案是给每个工作 Agent 定义输出 schema:成功/失败状态、关键数据字段、遗留问题列表。调度 Agent 拿到的是结构化 JSON,而不是一篇小作文。这个细节直接决定多 Agent 协作的上限。
5.2 下一步计划:浏览器自动化、向量记忆与开放评测集
项目目前的路线图上有三件事。第一件是把长期记忆从 Obsidian 文件库升级到真正的向量数据库,并加入自动摘要能力——文件库虽然人可读,但检索效率和语义匹配都还不够。第二件是完善 Harness 的可观测性:给每一步工具调用挂上 trace 可视化,让非技术人员也能看懂 Agent 在做什么。第三件是扩大评测集规模,目前两百多条任务还是太少,我打算引入社区共建评测集的方式,开放一些典型任务模板,让更多人一起覆盖边界场景。
工具层面,我最想补的是浏览器自动化能力,让 Agent 能真正“看”网页操作界面,而不只是通过 API 抓数据。这会把 Agent 的触达范围再往外推一步——真正意义上的 reach everything。至于多 Agent 之间的通信协议,我也考虑过。基于 HTTP 的回调会比较重,计划换成消息队列,让每个 worker 可以异步消费任务,调度 Agent 不必一直轮询等结果。这个改进预计能把并行任务的吞吐再提一两倍。
5.3 给新入坑的同学:Agent 开发学习路线怎么排
经常有人问我 Agent 学习路线怎么走,这里按我自己的复盘,给一条实践过的路径。第一阶段先把基础概念打通:prompt 工程、工具调用、RAG、上下文窗口,这些都是后续的地基。建议选一个现成 harness 把最小 Demo 跑通,先感受“模型调工具”是个什么手感。第二阶段开始拆架构,自己写一个简化版的主循环,重点理解模型输出怎么变成工具调用、工具结果怎么回灌上下文。第三阶段再做专项深挖:想搞生产化就学沙箱、权限、审计;想搞能力扩展就学 Skill 设计、记忆管理;想搞效率就做评测集、看板指标。
面试里高频考的点也基本落在这几条线上:ReAct 的原理和局限、工具调用的格式设计、上下文管理策略、Agent 与 RAG 的异同、评测集怎么构建、多 Agent 如何编排。与其背概念,不如把一个最小 Agent 亲手写出来,把这些环节都走一遍,比什么速成教程都管用。Agent 这个方向的门槛不在某一个算法,而在你能不能把模型、工具、状态、安全这些碎片高效地编排起来——这是纯工程能力,练得越多,手感越扎实。
Agent-Reach 从立项到现在,我做过最大的改动是砍掉第一版里所有花哨的东西:复杂的记忆网络、华丽的 UI、一长串预置技能,全部下架,换成一个干净的主循环和十来个稳定工具。事实证明,这套极简版才是能够持续演化的核心。项目走到这里,我最深的体会是:Agent 开发的第一性原理不是“让模型更聪明”,而是“给聪明一个可靠的执行环境”。模型负责天马行空,Harness 负责脚踏实地,两边都不缺位,Agent 才能真的干出活。
如果你也准备动手折腾自己的 Agent,我建议从最小闭环开始:一个主循环、三个工具、一条评测用例。跑通之后再一点一点加能力。别看市面上的框架和教程眼花缭乱,真正决定你的 Agent 能不能用的,永远是那些地基环节:可靠的循环、清晰的工具协议、严格的沙箱边界。先把这些夯实,后面自然水到渠成。