去年年底我在内部跑完一轮 Agent 项目复盘后把整套方案单独抽出来起名叫Agent-Reach。名字里的 Agent 不用解释Reach 才是我想表达的东西——英文里 reach 是“触达、够得着”但放到 AI Agent 的开发语境里它其实同时指三件事工具够得着、记忆够得着、编排够得着。这个项目解决的痛点也很具体Agent 的 Demo 人人都会跑但一旦放进真实业务里它经常“够不着”东西。先交代一下背景。当时我们手里已经有了不少 Agent 原型能聊天、能查资料、能调用外部工具演示时效果相当唬人。可一旦接到真实环境问题立刻暴露出来——网页抓回来一堆没用的 HTML、上下文被撑爆、工具权限没有边界甚至 Agent 会自己脑补一个不存在的 API 然后反复重试。这些问题不是模型能力不够而是外围工程没做好。Agent-Reach 就是围绕“触达能力”做的一整套连接层方案技能封装、沙箱控制、多 Agent 协作、评测回归。它不训练模型、不重造框架只解决 Agent 和真实世界之间的“最后一公里”。这篇内容适合正在做 Agent 开发、被框架选型和 Skill 设计折磨过的朋友。我会把 Agent-Reach 的架构思路、核心实现、选型取舍、踩坑过程从头到尾拆开讲里面很多细节是跑真实任务时一点点磨出来的希望能帮你少走点弯路。1. 为什么叫 Agent-Reach从“Demo 能跑”到“真正够得着”的差距1.1 一个让整个项目重写定位的失败案例先讲一个具体的失败案例。当时我们的任务是做一个“自动整理跨系统信息”的智能体从内部知识库查资料、从网页抓最新公告、然后汇总成一份周报。演示环境里一切正常——你问它一句它自己调工具、自己总结输出一段漂亮的文案。结果放进真实环境后成功率直接掉到三成左右。失败的原因拆开看特别有意思也和“触达”这个词高度相关。第一网页抓取工具返回的 HTML 原文太长模型上下文被无关内容占满跑着跑着就开始“失忆”忘掉最初的需求第二文件权限和网络权限没设计好它想读的文件读不到反而偶尔会去尝试调用一个根本不存在的内部接口第三多轮任务里缺少结构化的中间状态子任务做完的结果没有传递到下一步整个链路就断掉了。这三个问题都不是模型本身“不够聪明”而是 Agent 的触达链路太脆弱。当时我也试过换更强的基座模型但只能缓解不能根治。后来我想明白一件事Agent 的能力上限由模型决定但能力下限由外围工程决定。Agent-Reach 要做的就是把这条下限稳定地抬上去。1.2 Reach 的三层含义工具触达、记忆覆盖、编排纵深我给 Agent-Reach 定了三层核心语义整篇文章都围绕这三层展开。第一层是 Tool Reach工具触达。Agent 能调用多少外部能力以及这些能力是否可靠。比如能不能稳定地把网页转成干净的 Markdown、能不能读写指定的文件、能不能调用内部 API。这一层解决的是“手够不够长”的问题。很多 Agent 项目之所以平庸不是因为模型差而是工具层太薄、太脆动不动就返回一堆没解析过的原始数据。第二层是 Memory Reach记忆覆盖。任务级的上下文能覆盖多长关键信息在长任务中会不会丢。这包括短期窗口内的滚动摘要也包括跨会话的长期记忆。这一层解决的是“脑袋够不够用”的问题。我在实践中发现很多长任务失败不是模型不会做而是做到一半把前面的约束条件忘了。第三层是 Orchestration Reach编排纵深。多个 Agent 协作时指令能不能可靠地传递到最末端的执行层。谁拆解任务、谁调用工具、谁汇总结果这些流程要有一个清晰的协议而不是把所有上下文广播给所有人。这一层解决的是“团队够不够齐”的问题。1.3 Agent-Reach 不做什么拒绝自造 LLM 运行时定边界和定目标同样重要。Agent-Reach 一开始也走过弯路差点自己撸一套 Agent 运行时后来及时刹住了。原因很现实模型推理、流式输出、函数调用协议这些底层能力现有框架已经做得很成熟自己造一轮除了增加维护成本不会带来额外收益。所以这个项目的边界很明确不训练模型不重写推理引擎不重复造框架。它专注做连接层——把工具包装成可复用的 Skill给 Agent 加上沙箱和权限控制设计多 Agent 之间的协作协议再配一套评测集来验证“够得着没有”。这样做还有个额外好处换基座模型的时候整个外围工程可以原样迁移不需要重写。2. Agent-Reach 的整体架构Skill、Harness 与多 Agent 协作怎么分工2.1 先厘清一组概念Agent、Harness、Skill、Tool 分别管什么在做架构设计之前必须先厘清社区里经常混着用的几个概念Agent、Harness、Skill、Tool。概念管什么类比Agent决策主体负责任务理解、拆解和生成最终回答项目组里的“负责人”Harness承载 Agent 运行的运行时负责循环控制、上下文管理、工具路由负责人的“工作台”Skill可复用的能力单元封装了工具调用方式、参数说明和使用指令工作台上“即插即用的接头”Tool最底层的函数/接口调用比如抓网页、读文件、调 API接头连接的“外部设备”很多人问 harness 和 agent 到底有什么区别我用自己的话总结Agent 是“脑子”负责想Harness 是“身体”负责让脑子想的事情能执行下去。没有 harnessAgent 就是一个光会说话不会干活的聊天窗口没有 Skillharness 里的工具就是一盘散沙每次都要在提示词里重新解释一遍怎么用。2.2 Agent-Reach 的四层架构模型层、调度层、技能层、执行层Agent-Reach 的运行时可以简化成四层每一层只做自己该做的事。模型层负责自然语言理解、推理和工具选择判断。这一层是可以替换的我们内部接入了多个基座模型做对照发现只要外围工程到位不同模型之间的差距会被明显拉近。调度层也就是 harness 核心。它负责 Agent 主循环、上下文管理、记忆压缩、技能发现与路由。举个例子模型说“我需要把某个网页存下来”调度层就去技能注册表里找 description 匹配的 Skill然后按 Skill 定义的参数格式去调用。技能层存放所有 Skill。每个 Skill 是一个独立单元包含描述文件、脚本、依赖和测试。技能层不关心模型怎么想只保证“你按我的规则调用我就给你稳定的结果”。执行层真正触碰外部系统的地方包括沙箱、网络白名单、文件读写网关。这一层也是安全边界所有工具调用都在这里被审计和约束。结构上四层之间是严格向下的依赖关系模型层不能直接触碰执行层必须经过调度层和技能层。这样设计的好处是任何一次工具调用都能被记录、被控制、被回放出了问题可以直接从审计日志里定位。2.3 为什么把 Tool 封装成 Skill而不是直接挂一堆函数最初版本里我们把所有能力直接做成 Python 函数列表塞给模型结果维护起来非常痛苦。每加一个工具就要改代码、改提示词、重新部署而且模型经常分不清两个功能相似的工具到底该用哪个。后来参照社区里 Agent Skills 的思路重构成了 Skill 机制。Claude 最近的 Agent Skills 设计就很典型——把技能拆成 SKILL.md 这样的描述文件模型按需加载。Agent-Reach 基本沿用了同样的包装方式原因很简单让模型自己判断什么时候用哪个工具而不用开发者在代码里写死调用链。Skill 封装还有一个关键价值每个 Skill 可以独立测试。你不用为了验证一个网页抓取功能就把整个 Agent 跑一遍。改完 Skill A只跑 Skill A 的测试用例就行。这对后续做评测集回归非常重要。3. 核心实现让 Agent 触达网页、文件与下游 API3.1 Skill 的目录结构与加载机制Agent-Reach 里的每个 Skill 都长这样skill_save_webpage/ ├── SKILL.md ├── src/ │ └── save_webpage.py ├── requirements.txt └── tests/ └── test_save_webpage.pySKILL.md 是技能的描述文件也是模型能否正确调用它的关键。我们内部约定这个文件必须包含三块内容name用来做唯一标识description用两三句话说明这个技能能做什么、适合什么场景parameters定义参数名、类型、是否必填。description 写得好不好直接决定模型在意图识别阶段能不能找到这个技能。--- name: save_webpage description: 将指定 URL 的网页正文抓取并保存为 Markdown 文件适用于资料收集、简报生成、网页内容存档 version: 1.2.0 parameters: - name: url description: 目标网页地址 type: string required: true - name: output_dir description: 保存目录默认为 /data/write/webpages type: string required: false ---调度层启动时会扫描技能目录把每个 SKILL.md 的 name、description、version 注册进技能表。模型只需要看到这张“技能菜单”就知道自己有哪些能力可用真正执行时才把对应脚本拉起来。这种延迟加载的方式也能省下不少上下文空间。3.2 实战把网页保存成 Markdown 的 Skill这个 Skill 是 Agent-Reach 里最常用的一个对应的需求也很典型让 Agent 将网页保存成 Markdown。核心思路是三步抓取页面 → 提取正文 → 转成 Markdown。为什么要用正文提取而不是直接存 HTML因为 HTML 里有大量导航、广告、脚本标签这些东西对模型来说全是噪音塞进上下文就是灾难。import argparse from urllib.parse import urlparse import trafilatura from markdownify import markdownify as md def save_webpage(url: str, output_dir: str) - str: downloaded trafilatura.fetch_url(url) if not downloaded: return ERROR: 页面抓取失败请确认 URL 可访问 content trafilatura.extract(downloaded, include_linksTrue) if not content: return ERROR: 正文提取为空可能是页面需要渲染或内容被反爬拦截 filename urlparse(url).netloc _ str(abs(hash(url))) .md md_content md(content) with open(f{output_dir}/{filename}, w, encodingutf-8) as f: f.write(md_content) return fOK: 已保存到 {filename}{len(md_content)} 字符 if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--url, requiredTrue) parser.add_argument(--output-dir, default/data/write/webpages) args parser.parse_args() print(save_webpage(args.url, args.output_dir))这里有一个非常关键的细节工具返回给模型的文本必须短。我们只返回“文件路径 字符数”而不是把整篇 Markdown 塞回去。模型知道文件存在、在哪、多大就已经足够继续决策了如果后续需要基于全文修改它可以再调用一个read_file的 Skill 按需读取。这个设计决策避免了 90% 的上下文溢出问题。3.3 记忆模块短期缓冲与长期持久化Agent 的记忆能力是“够不够得着”的第二层。我给 Agent-Reach 设计了短期和长期两级记忆分工完全不同。短期记忆负责运行期内的上下文管理。当一个任务跑了很多轮、历史消息快要把上下文窗口占满时调度层会自动触发压缩把早期的对话交给一个摘要模型压缩成结构化要点然后替换掉原始消息。# 简化后的滚动摘要逻辑 tokens_used estimate_tokens(messages) if tokens_used / context_limit 0.8: early_messages messages[:-20] summary summarizer.compress(early_messages) messages [{role: system, content: f早期对话摘要{summary}}] messages[-20:]长期记忆则负责跨会话的信息复用。任务结束后Agent-Reach 会把目标、关键决策、产物路径写入一个本地索引我们用 JSON 文件加简单向量检索就够用了没上重型数据库。下次遇到同类任务时调度层会先检索历史记录把相关经验注入系统提示词让 Agent 不必从零开始摸索。我记得有个很典型的例子第一次让 Agent 整理某个内部系统的数据时它费了好大劲才搞明白字段含义第二次再跑类似任务长期记忆里已经有字段映射说明整个任务的时间缩短了将近一半。这就是记忆覆盖带来的直接收益。3.4 多 Agent 协作时的共享上下文设计Agent-Reach 在复杂任务里会拉起多个子 Agent 协作比如一个负责抓取资料、一个负责分析数据、一个负责生成报告。最开始我们图省事把完整对话历史复制给每个子 Agent结果 token 消耗直接翻了三倍而且子 Agent 经常被不相关的讨论带偏。后来改成了“任务单”模式主编 Agent 负责拆解任务为每个子 Agent 生成一张独立的任务单里面只包含它需要的输入字段、目标和输出位置。子 Agent 完成后回传结构化的结果摘要主编 Agent 再把所有摘要汇总成最终输出。任务单子 Agent 视角 - 角色资料抓取员 - 输入url 列表 [https://..., https://...] - 目标将每个 URL 转为 Markdown存入 /data/write/webpages/ - 输出格式JSON [{url, file_path, char_count}] - 约束不修改已存在的文件不访问外网这个设计和上下文管理是同一个思路每个人只需要知道自己该知道的部分。多 Agent 协作的失败绝大多数不是因为模型能力而是信息传递方式太粗暴。4. 框架选型实录LangChain、Dify、CrewAI 到底怎么选4.1 三个框架的边界与性格差异网上关于 LangChain、Dify、CrewAI 哪个好的讨论非常多我的回答是它们根本不是同一个层面的东西硬比没有意义。用我之前一张整理过的对照表来说明框架定位上手成本适合场景主要短板LangChain / LangGraphLLM 应用开发框架中高完全自定义的编排逻辑、复杂状态机抽象层级多版本更新快学习曲线陡DifyLLMOps 应用平台低快速搭建知识库、工作流、RAG 应用深度定制受限复杂 Agent 逻辑绕CrewAI多 Agent 协作框架低中角色化任务拆解、剧本式协作流程复杂状态流转不够灵活简单说LangChain 是“乐高积木”什么都能拼但你要自己看说明书Dify 是“精装房”拎包入住很方便但想改承重墙就麻烦CrewAI 是“剧组管理工具”帮你把不同角色组织起来拍戏但剧本太复杂它也头疼。4.2 Agent-Reach 的最终选型与理由Agent-Reach 的实际选型可以用一句话概括工具层完全自研多 Agent 剧本用 CrewAI复杂状态流才用 LangGraph业务前台可以接 Dify。工具层和沙箱自研是因为我们需要精确控制权限、审计日志和评测插桩。框架里现成的工具调用机制通常做得比较薄很难满足安全约束和评测回归的双重需求。与其在框架里打补丁不如自己维护一层不重的工具网关。多 Agent 协作层用 CrewAI是因为我们的需求很契合它的设计理念几个角色各有分工按固定剧本协作。CrewAI 的 role、task、process 模型用起来很直观模型只需要知道自己是什么角色、该完成什么任务。但如果哪天需要跑带条件分支的复杂状态流我会切到 LangGraph——它是图结构的能显式表达状态转移而不是靠提示词硬撑。至于 LangChain 全家桶我的态度是“按需引入不搞全家福”。它在链式调用和工具封装上有成熟的地方但没必要为了用而用。框架应该是手段不是信仰。4.3 Rust Agent 方向为什么值得持续关注热词里有一类“基于 Rust 语言 AI Agent”的话题我也简单说下我的观察。Rust 在 Agent 领域最大的优势是类型安全、内存安全、性能好特别适合写工具网关、沙箱管理和高并发请求转发这类对资源隔离和稳定性敏感的组件。Python 里很多要到运行时才暴露的错误在 Rust 编译期就被挡住了。但也要客观地说目前 Agent 编排层的生态仍然是 Python 的天下大部分现成的 Skill、工具库都长在 Python 生态里。Agent-Reach 的折中方案是外围用 Python 写编排和技能工具网关这类高并发组件用 Rust 单独维护。这样既能吃到 Rust 的性能红利又不会被过早地带进“全量重写”的坑。我建议关注 Rust Agent 方向但不必一步到位。5. 沙箱与安全能力半径越大约束就要越具体5.1 为什么每个外部动作都要过一道沙箱谈到 Agent 安全很多人第一反应是防“恶意 Agent”。但我在实际项目里最大的体会是真正的风险不是恶意而是幻觉加上过度自信。模型可能误以为某个命令是对的就真的去执行可能把内部 API 的地址记错了就反复去请求可能读到了一个文件就顺手把它写回了错误的位置。所以 Agent-Reach 里每一个外部动作都必须经过沙箱。沙箱不是要限制 Agent 的创造力而是给它一个透明的玻璃房间——在里面怎么折腾都行但接触外部世界必须走受控的门。没有门就没有审计没有审计出了问题你连从哪查都不知道。5.2 网络、文件、命令三个维度的权限模型我们的沙箱权限模型只做三件事网络白名单、文件读写分区、命令限制。维度默认策略说明网络默认禁止任意出站只允许白名单域名按业务配置允许访问的域名列表其余请求一律拦截文件只读目录与可写目录分离参考数据挂只读任务产物只能写入指定工作目录命令命令白名单 超时 资源限额禁止高危命令限制 CPU、内存和单次执行时长这三条规则配合起来基本上能防住我在项目里遇到过的绝大多数问题。比如之前那个“网页转 Markdown”的 Skill它只允许访问目标网址所在的几个域名只允许往/data/write/webpages/下写文件不允许执行任意 shell 命令。这样即便模型抽风想要做点什么出格的事也会在沙箱这层被拦下。5.3 一套可复用的沙箱配置示例Agent-Reach 用的是容器做执行层隔离。下面是一份可以直接抄的 docker-compose 配置示例services: agent-exec: image: python:3.11-slim read_only: true tmpfs: - /tmp:size100m volumes: - ./data_read:/data/read:ro - ./data_write:/data/write:rw cap_drop: - ALL security_opt: - no-new-privileges:true mem_limit: 1g cpus: 1.0 network_mode: none如果某个任务确实需要访问外部 API我会把network_mode: none换成自定义网络并配置统一出口网关只放行白名单域名的 DNS 解析和连接请求。看起来更复杂但这就是 Agent 触达外部世界必须要付出的安全成本。密钥处理上也有一个规矩所有 API Key、Token 一律通过环境变量注入容器内代码不要把它们打回日志。工具返回给模型的文本里绝不允许出现密钥内容这一点要在 Skill 的代码规范里写死靠提醒不可靠要靠约定和审查。6. 评测集与回归测试怎么证明 Agent-Reach 够得着6.1 通用知识评测集不能替代业务评测集很多团队验证 Agent 的方式是跑一遍通用知识类评测集看个分数就完事。我的看法是通用评测集测的是模型的知识储备和基础推理能力它根本测不出 Agent 的“触达能力”。网页抓取成功率高不高、工具调用参数对不对、多轮任务里上下文保持得稳不稳这些问题在通用评测集里一个都答不了。所以 Agent-Reach 单独建了一套业务评测集。办法很朴素从真实业务里挑出 20 到 50 个代表性任务做成固定用例每个用例带上期望动作序列和期望产物。这套评测集跑一次比跑十个通用榜单都有说服力。6.2 评测维度设计与用例样例我们内部评测主要看五个维度维度指标采集方式任务完成率golden 判定通过/失败检查最终产物是否符合预期工具调用正确率调用的工具名、参数是否正确审计日志自动比对步骤合规率是否违反权限策略/越权操作沙箱日志自动比对上下文保持率长任务中是否遗忘原始目标人工抽检 关键信息点比对成本与效率端到端耗时、token 消耗运行时监控自动采集用例的 JSON 格式大概是这样的{ id: case_007, task: 把 https://example.com/blog/agent-reach 保存成 Markdown放到 /data/write/webpages/ 下, expected_actions: [save_webpage], expected_actions_order: [save_webpage], expected_file: example.com_xxx.md, must_not_actions: [shell_exec, delete_file, write_to_read_dir] }执行时会记录完整的动作轨迹和 expected_actions 做比对同时检查 must_not_actions 里有没有违规。这个设计特别适合防回归——改了一个 Skill 之后最怕的就是别的用例跟着挂。6.3 把评测跑进 CI每次改 Skill 自动回归评测集如果没有融入开发流程很快就会变成摆设。Agent-Reach 的规矩是任何 Skill 改动必须过评测集。我们把评测脚本挂到了 CI 流水线里每次 push 代码、更新 Skill 版本或调整提示词就自动触发全量回归。回归流程分三步先重新构建沙箱环境确保依赖干净然后逐条执行评测用例记录每个动作日志最后生成一份报告列出各维度的得分和失败用例的完整轨迹。失败时可以直接看到是哪个环节出了问题——模型选错工具、参数格式不对、还是沙箱拦截了本不该拦的操作。这套评测集建好之后我最大的感受是Agent 开发终于从“感觉差不多能用”变成了“有数据证明这个改法有效”。也正因为有这套回归后面几次调整记忆压缩策略时才敢放心地改代码不用整天提心吊胆怕弄坏旧功能。7. 落地过程中踩过的坑与排查思路7.1 工具返回超长导致上下文漂移一次完整的排查链路这个坑几乎是每个 Agent 项目都会踩的。我们的现象很典型任务跑到三分之一之后模型开始胡言乱语甚至引用一些上下文里根本不存在的“事实”。排查过程是这样的。第一步我打开审计日志检查最近一次工具调用的返回体发现网页正文八万字符被原封不动塞回了上下文。第二步粗算了一下 token 开销八万字符大概两万 token已经把上下文窗口吃掉一大半。第三步进一步对比正常用例和失败用例发现失败任务的上下文在工具调用后 token 数出现断崖式上涨而工具返回内容恰好包含了大量与任务无关的正文。第四步定位根因Skill 把“全文”当成返回值而不是只返回“摘要和文件路径”。第五步的修复就是改返回策略——只回传文件名、路径和字符数。第六步验证同一个用例连续跑五次上下文保持率从不到五成升到了九成以上。这个坑的启示是工具返回给模型的文本本身就是一个信息通道通道设计不好模型再聪明也没用。以后所有 Skill 定义返回值时我都会先问一句这个返回内容真的需要模型逐字阅读吗7.2 模型幻觉式调用工具它调了一个根本不存在的 API另一个高频问题是模型调用了一个根本不存在的工具或 API。日志里显示它用了一个看起来很像那么回事的端点还带着合理的参数重试了三次才放弃。我第一反应是代码里有 bug检查半天发现工具注册表里压根没有这个工具——这些调用完全是模型从上下文里的只言片语“脑补”出来的。对策有三条。第一在工具网关层面做 schema 校验模型调用的端点必须存在于注册表不认识的一律返回“NO_SUCH_TOOL”而不是含糊的 404。第二在系统提示词里明确列出现在不提供哪些能力让模型知道边界在哪。第三给重试策略加上限和退避连续失败三次就强制换思路。这个坑让我意识到Agent 的“自信”和“幻觉”是双刃剑工程上必须用一个不依赖模型自觉的校验层兜底。7.3 多 Agent 协作时的 Token 消耗失控前面提到过最初给多个子 Agent 复制完整上下文的方案让 token 消耗直接翻了三倍。我一开始还以为是哪里配置重复了后来逐个 Agent 打印消息列表才发现——每个子 Agent 都重新读了一遍完整的历史对话而它真正需要的只是自己那部分输入。修复方案就是“任务单”模式每个子 Agent 只拿自己需要的输入槽位只回传结构化结果而不是原始对话。改完之后同样任务的总 token 消耗下降了六成左右子 Agent 的输出质量反而更高了因为它不再被无关内容干扰。这个优化对成本的影响非常直观一个月下来 API 账单的差别很明显。7.4 Skill 命名与版本管理的冲突最后一个坑是版本管理。有一次我更新了网页保存 Skill 的行为——调整了输出目录结构——但旧版本 Skill 还在注册表里躺着模型有时候命中新版、有时候命中旧版行为表现完全不一致。查了半天才发现是技能缓存的锅。后来定了一条硬规矩SKILL.md 里的 nameversion 必须在注册表里唯一版本变更时要清理旧缓存调度层启动时校验所有 Skill 的描述文件发现同名的旧版本直接标记为失效。现在每次更新 Skill 都会自动触发一轮评测回归确认只有新版本在生效。这些坑单看每一个都不算复杂但它们组合在一起就是 Agent 从“实验室玩具”变成“生产工具”之间的全部距离。我在实际项目中最大的体会是Agent-Reach 这个项目的价值不在于某个亮点技术而在于把工具触达、记忆覆盖、编排纵深、安全边界和评测闭环这些看似琐碎的工程细节全部串了起来。同事后来调侃说Reach 与其翻译成“触达”不如翻译成“伸手去够”——我觉得挺好工程上做 Agent本来就是在不断把够不着的东西够到手。下一步我打算继续打磨网页转 Markdown 这个 Skill让它能处理动态渲染的页面同时把评测集从几十个用例扩展到一百个。