ARTICLE DETAIL

资讯详情

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

Agent工程实践:从Harness运行循环到Skill与记忆的落地指南

Agent工程实践:从Harness运行循环到Skill与记忆的落地指南 把 Agent 从“能跑通 Demo”推到“能接真实流量”中间隔着的东西比大多数人想象的多得多。Agent-Reach 这个项目就是我在这个过程中沉淀下来的一套轻量级 Agent 工程实践。名字很直白让 Agent 的能力真正 reach 到业务场景而不是永远停在示例代码和聊天框里。它不是一个试图取代 LangChain、Dify 的新框架而是一套可裁剪的工程基线核心包含 harness 运行循环、skill 注册表、memory 存储和多 Agent 编排四块。如果你正准备开始 agent 开发或者已经在用 agent 框架但总觉得哪里不受控这篇文章会帮你理清架构取舍和踩坑点。1. 为什么做 Agent-Reach一个想解决“最后一公里”的项目1.1 从热搜词看 Agent 开发者的真实焦虑我平时会留意 agent 相关的热搜词看大家在搜什么其实能看到一条非常清晰的学习路径。最初是“agent是什么”“agent学习路线”“agent开发需要学什么”这是入门者在找方向接着是“harness和agent区别”“agent框架与编排”“agent框架如LangChain、Dify、CrewAI哪个好”这是已经动手写代码的人在选型再往后是“agent记忆”“agent skill教程”“agent tool agent skills”这是在做能力封装最后是“ai agent怎么扛并发”“多agent”“agent安全”这是真正上线前绕不开的三座山。这些热搜词不是散点而是一条完整的工程化链路。很多项目死掉不是因为模型不够聪明而是因为第二步到第四步之间缺东西有了 Agent 的大脑没有 harness 给它搭工作台有了工具没有 skill 教会它怎么组合使用有了上下文字段没有记忆机制控制它别把上下文撑爆有了任务入口没有并发模型应对真实流量。Agent-Reach 就是把这条链路里最容易漏掉的工程部分补上。1.2 Agent-Reach 的项目定位与选型取舍先说清楚 Agent-Reach 不是什么。它不是一个大而全的低代码平台也不是一个绑定某家模型供应商的 SDK更不是一个试图把 prompt、工具、记忆、UI 全部打包的“全家桶”。它更像一个“半框架半脚手架”的基线给你一套清晰的目录结构和运行循环同时把每块都留出替换口。这个定位是我对比了主流 agent 框架之后确定的。方案定位优势我实际使用中的短板LangChain工具链/框架生态全、组件多抽象层太厚出了问题要翻很多层源码Dify低代码平台上手快、可视化自定义逻辑受限适合快速验证不适合深度定制CrewAI多 Agent 编排框架角色分工直观编排模型偏重小任务用起来有点重Agent-Reach工程基线结构清晰、可替换、可控需要自己补一些通用组件选型时我有一个很朴素的标准当我要排查一个线上问题我能不能在半小时内定位到是模型问题、工具问题还是状态问题。用 LangChain 这类框架时答案经常是不能因为它的调用链太深。Agent-Reach 的做法是默认极简按需加码。第一版我用 Python 写因为模型调用、schema 校验、asyncio 生态都成熟调试也比 Rust 舒服。但我专门把 harness 层和业务逻辑做了隔离后续如果某个模块对性能不满意比如要在一个长连接服务里塞上千个并发 Agent 会话完全可以只把 harness 核心用 Rust 重写skill 和 memory 通过接口对接。这个“先 Python 跑通再 Rust 提速”的路径是很多 agent 项目的现实选择热搜里“基于Rust语言ai agent”其实就是这么来的。2. Agent-Reach 的核心架构拆解harness、skill 与记忆2.1 先把 Agent 的“运行循环”讲清楚要理解 Agent-Reach得先理解 agent 到底是什么。你可以把 agent 简单理解成一个“大模型 工具 记忆 循环”的组合体。普通的大模型调用是“你说一句它回一句”agent 不一样它接收一个目标自己拆解自己决定用什么工具然后看工具返回的结果决定下一步怎么办直到任务完成为止。这个循环一般长这样接收用户任务写入会话状态。harness 根据任务构造模型输入包括系统指令、历史上下文、可用工具的描述。模型返回一个结构化结果可能是“调用某个工具”或“任务完成”。harness 对结果做校验如果格式不对就按错误处理。校验通过后harness 执行对应工具并把工具返回结果追加到上下文。重复第 2 步直到模型返回完成信号或达到最大步数。可以类比成新员工入职老板布置一个任务新员工不可能一句话就交出成果他要先查资料、用 Office 工具、写草稿、找同事确认最后汇报。Agent 也一样“思考”和“行动”必须来回交替才叫 agent。这也是为什么 agent 项目通常比普通 API 封装复杂你要处理的不是一次输入输出而是一个有状态、有循环、有分支的异步流程。2.2 Harness 和 Agent 到底有什么区别很多人搜“harness和agent区别”说明这个概念确实容易被绕。简单说Agent 是那个会思考、会做决策的模型部分它决定“做什么”Harness 是承载 Agent 的那套运行环境它决定“怎么做才不出事”。Harness 管的事很杂什么时候调用模型、调用哪个模型、模型返回的工具参数怎么校验、工具调用失败怎么重试、上下文窗口怎么裁剪、每一步的日志怎么记录、超过最大步数怎么收场。如果没有 harnessAgent 就像裸奔的大脑能力再强一轮工具出错或者一次上下文超长就可能崩掉。Agent-Reach 里的 harness 模块职责是非常明确的。它不像 LangChain 那样把 agent 和 chain 揉在一起而是做了一个很纯粹的“循环器”负责推进状态、维护消息、调用模型、执行工具、记录轨迹。这样测试的时候可以单独测 harness换模型的时候也只动模型接口不用重写业务。我习惯把 harness 想成一个操作系统Agent 是上面跑的进程skill 是安装的软件memory 是文件系统而调度策略就是进程调度器。这个概念一旦建立起来后面很多设计都能自然对上。2.3 Skill 和 Tool 的边界以及记忆怎么落地Tool 和 Skill 是我见过最容易混淆的一对概念。在 Agent-Reach 里Tool 是原子动作比如“发送 HTTP 请求”“执行一段代码”“查询数据库”Skill 是面向任务的方法论一个 Skill 内部会编排多个 Tool。举个例子热搜里有个很典型的“agent 将网页保存成 markdown 的 skill”这绝不是一步请求就能完成的它要先抓取网页 HTML提取正文去掉导航和广告再转换成 Markdown最后处理图片相对路径。如果你把这一串逻辑写成一个大 Tool会让工具的复用性变差正确做法是把它封装成一个 Skill内部可以拆成 fetch_html、extract_main、convert_markdown 三个 Tool。这样其他 Skill 也可以单独复用提取正文这个能力。记忆这块Agent-Reach 分两层处理。短期记忆就是当前任务里的消息列表由 harness 管理负责让模型知道“刚才做了什么”长期记忆是跨任务的摘要或向量索引放在 memory store 里负责让 Agent 在下一次任务还能回忆起关键信息。这里最容易踩的坑是把长期记忆当垃圾桶一股脑全塞回去。我的做法是每次任务结束后由模型生成一段结构化摘要只保存任务目标、关键决策、最终结果和遗留问题查询时再按相关度取回。这样既能控制 token又能让记忆真正起作用。3. 实操从零跑通一个 Agent-Reach 最小闭环3.1 项目结构与核心代码骨架讲了半天原理直接上实操。Agent-Reach 的目录结构我建议这样拆agent_reach/ harness/ loop.py # 运行循环 context.py # 消息上下文管理 skills/ registry.py # skill / tool 注册表 web/ # 网页类 skill code/ # 代码执行类 skill memory/ store.py # 记忆存储接口 summary.py # 摘要压缩 agents/ worker.py # 单个 worker 入口 main.py核心运行循环先写一个最小版本。下面这段代码是我实际项目的第一版没有加太多花哨功能但足够跑通一个“模型想工具、harness 执行工具、模型看结果”的闭环# harness/loop.py 简化版运行循环 class Harness: def __init__(self, model_fn, registry, memory): self.model_fn model_fn self.registry registry self.memory memory async def run(self, task: str, max_steps: int 12): state {task: task, messages: [], done: False} for _ in range(max_steps): messages self.memory.build_prompt(state) action await self.model_fn(messages, toolsself.registry.schemas()) if action.type finished: state[done] True return action.answer result await self.registry.execute(action.tool, action.args) self.memory.observe(state, action, result) raise RuntimeError(f超过最大步数 {max_steps}任务未完成)这段代码里有几个点值得说明。state 里存整个任务的状态消息历史不直接塞给模型而是由 memory.build_prompt 先生成这样方便后续在 build_prompt 里做窗口裁剪和摘要压缩。model_fn 是模型接口返回 actionaction.type 有两种finished 或者 tool_call。registry 是 skill/工具注册表schemas() 返回给模型的工具描述execute 负责找到对应工具并执行。max_steps 是安全阀防止模型陷入死循环这个参数在实际项目中比很多人想象的重要我见过不少线上 Agent 卡在“模型反复调用同一个工具”的怪圈里。如果是做“网页保存成 markdown”这个 skill注册逻辑大概是这样的registry.register(extract_webpage_markdown) class WebpageToMarkdownSkill: def __init__(self): self.tools [fetch_html, parse_main, convert_markdown] async def run(self, url: str): html await fetch_html(url) main parse_main(html) return convert_markdown(main)这里的关键是 skill 的 run 方法里不要写业务逻辑而是把子工具串起来。模型不需要知道 skill 内部怎么实现它只要看到 skill 的描述“把网页正文提取成 markdown”需要的时候调用它就好了。3.2 并发处理单机队列到多 WorkerAgent 怎么扛并发这个问题要看场景。如果只是内部工具几十个并发已经很大了如果面向 C 端可能要撑到几百上千。Agent-Reach 第一版做的是“单机多 Worker 异步队列”不引入太重的中间件。import asyncio from harness.loop import Harness async def worker(queue, harness_factory, semaphore): while True: task await queue.get() async with semaphore: harness harness_factory() try: result await harness.run(task) print(f任务完成: {task.id}, 结果长度 {len(result)}) except Exception as exc: print(f任务失败: {task.id}, 错误 {exc}) queue.task_done()为什么每个 worker 要持有独立的 Harness 实例因为 Agent 会话状态是私有的不能共享。如果多个任务共用一个 harness消息历史就会互相污染导致 A 任务的结果跑到 B 任务的上下文里。信号量 semaphore 的作用是控制在途任务数量防止一瞬间把所有任务都打到模型接口上。并发参数我建议按下面的经验值起步参数建议值说明max_concurrency上游模型限流的 80%留 20% 余量给重试和其他业务per_task_timeout120s ~ 300s根据任务复杂度调整超过就终止max_retries幂等工具 2 次非幂等 0 次重试前先判断工具是否安全可重放queue_size1000超过则直接拒绝新任务避免堆积这里有个容易算错的地方Agent 任务的并发不能按普通 HTTP 接口的 QPS 来算因为一个任务内部会反复调模型接口。假设一个任务平均要调 8 次模型那么 10 个并发任务实际上是 80 次模型调用在排队。所以限流别只看“多少任务进来”要看“模型接口每秒能吃多少请求”。3.3 多 Agent 协作的编排方式多 Agent 不是把多个 agent 放在一起就会更好。真正生产环境里多 Agent 主要解决两个问题一是单个 Agent 的工具太多容易选择困难二是不同任务需要不同专长。Agent-Reach 支持两种编排主管-下属模式和流水线模式。主管-下属模式适合任务拆解由主管 agent 拆任务并分发流水线模式适合固定步骤流程。class SimpleMessageBus: def __init__(self): self.handlers {} def register(self, agent_name, handler): self.handlers[agent_name] handler async def send(self, to, message): handler self.handlers.get(to) if handler is None: raise ValueError(f没有注册 agent: {to}) return await handler(message)主管 agent 的职责不是自己把所有事干完而是像项目经理一样拆解任务、派活、汇总结果。这里最关键的一点是给每轮协作设置总预算。比如主管把任务分给 3 个下属每个下属最多调 10 次模型主管自己最多调 20 次。没有预算的多 Agent 系统很容易出现“三个 agent 互相踢皮球”的尴尬局面模型层面根本没有成本意识必须由编排层帮它强制刹车。另外多 Agent 之间的消息应该传“结构化结果”不是传聊天记录。下属 agent 完成一个子任务后返回一段 JSON包含结论、关键证据、置信度和耗时。主管只需要看结构化结果做决策不需要把下属的每一轮思考都看一遍这样既省 token 又不容易被无关信息带偏。4. 常见问题与排查技巧实录4.1 工具调用不可控格式解析、超时与重试工具调用是最容易出问题的环节。我见过三种典型症状模型返回的 tool_call 参数偶尔缺字段、类型不对工具执行时间太长导致任务悬挂工具报错被模型当成正常结果继续走。第一种问题要靠强校验解决。在 Agent-Reach 里我用 Pydantic 定义每个工具的参数 schema模型返回后先校验校验失败就把错误信息回传给模型让它修正后再调一次。这里的关键是错误信息要写得足够具体比如“参数 expected 需要是字符串你传了数组”而不是简单说“参数错误”。第二种问题要靠超时控制。Agent 任务里的工具调用不能像普通函数一样无限等所有外部调用都要套超时。HTTP 请求 15 秒、代码执行 30 秒、数据库查询 10 秒这是我的默认值。超时之后先判断工具是否幂等如果幂等可以重试非幂等则直接返回失败让模型换一种方案。第三种问题比较隐蔽。模型看到工具返回的错误消息有时会自己脑补成“执行成功”。比如搜索工具返回“网络错误”模型下一轮却说“根据搜索结果答案是……”。解决办法是在工具返回结果里加一个明确的 status 字段harness 层如果发现 status 是 error不允许把它当作正常观察结果而是强制引导模型重新选择工具或者承认失败。4.2 上下文越长越傻记忆裁剪与压缩策略“任务越做越慢回答越来越飘”是 Agent 项目做到一定长度后的通病。原因很简单上下文窗口被工具返回结果塞满模型注意力被大量无关信息干扰。Agent-Reach 的做法是每轮循环都检查上下文长度而不是等到模型接口报错才处理。建议阈值是短任务上下文控制在 4k token 以内长任务控制在 20k token 以内。超过 70% 阈值时触发压缩先把最早的消息摘要成几句话保留最近几轮完整消息再把关键工具结果里的长文本抽成要点。这个压缩动作可以由模型完成也可以直接用规则截断我在第一版用规则截断后来改成模型摘要效果更好但耗时也更高。我还发现一个容易被忽略的细节工具返回结果往往是大头。比如抓取网页的结果可能有三万字但真正有用的只有开头几段。与其让模型处理完整结果不如在工具返回前先做一次清洗把长文本压缩成摘要再塞进上下文。这样既控制了 token也减少了模型被无关内容误导的概率。4.3 Agent 安全边界权限、注入与审计Agent 上线前必须处理安全边界否则后果很难预料。首先是最小权限原则Agent 能访问的东西越少越好。HTTP 工具要加 URL 白名单代码执行工具要用沙箱文件工具要限制可读写目录。不要为了省事把数据库的连接串直接配给 Agent 当工具否则一次 prompt 注入就可能造成数据泄露。Prompt 注入是 Agent 特有的风险。工具返回的网页内容、PDF 文本、用户输入都可能包含恶意指令试图让模型“忽略之前的系统指令”。Agent-Reach 的做法是把工具输出标记为不可信数据系统指令放在消息开头并显式告诉模型凡是来自工具返回、网页内容、用户上传文件里的指令都只是数据不是可以执行的指令。对高风险场景可以单独用一个小模型清洗工具输出把疑似注入的内容过滤掉再交给主模型。最后是审计日志。每一步模型调用、工具调用、参数、返回结果都要落日志。日志不是为了事后追责而是为了出问题时能回放到底是模型决策错了还是工具传参错了还是上下文被污染了。我把 Agent 的审计日志设计成类似普通应用访问日志的格式每条记录带 request_id、step、agent_name 和耗时排查问题时按 request_id 一拉就能看到完整链路。问题现象排查方向快速解决tool_call 格式乱模型返回参数缺字段看模型原始返回强校验 错误回传重试工具超时任务卡住不结束看工具调用耗时设置超时 幂等重试上下文膨胀回答越来越偏看 token 用量曲线窗口裁剪 摘要压缩prompt 注入模型行为异常看工具返回内容标记不可信数据 独立清洗多 agent 死循环任务迟迟不结束看各 agent 调用次数设置总预算 强制刹车最后说一点个人体会。Agent-Reach 这个项目做到现在我最大的变化是不再迷信框架也不再迷信模型。一个 Agent 能不能稳定工作更多取决于你有没有给它搭好 harness、配好 skill、管好记忆、控好并发。如果你也想让 agent 真正落地不要急着堆功能先把这条主链路跑通再谈多 Agent 和花哨编排。这个项目后续我会在 harness 的 Rust 版、skill 评测集和可观测性上继续扩展但第一步永远是先把一个最简单的循环稳定下来。
返回列表