当前位置: 首页 > news >正文

30分钟手搓 Agent:LLM + Tools + Loop + Memory 跑通最小闭环

Agent 的最小执行链,到底长什么样?

想了想,最好的办法就是手搓一个。

先不管 ReAct、MCP、Function Calling、Memory、Harness 这些词。先不讲“自主规划、自主执行、自主反思”。

就写一个最小 Agent。

它只做四件事:

  • 读用户任务;

  • 让模型选择下一步;

  • 按模型要求调用工具;

  • 把工具结果喂回去,继续循环。

如果 30 分钟只能写这么一个小东西,它当然不是 Claude Code,也不是 OpenClaw。

但我觉得它很适合当一堂解剖课。

写出来的是一个小玩具,看清的是 Agent 工程的骨架。

等这个最小闭环跑起来,再回头看 Claude Code、OpenClaw、Harness,就会顺很多:它们没有把 loop 写得多玄,更多是在 loop 外面一层层补上工具边界、上下文边界、记忆边界、权限边界和验证边界。


这次只做一个最小 Coding Agent。

它有三类输入:

  • 用户任务;

  • 当前对话历史;

  • 工具列表。

它只有三个工具:

  • list_files(path):列目录;

  • read_file(path):读文件;

  • run_command(command):运行一条白名单命令。

先不给写文件工具。

这个选择有点保守,但更接近我自己的使用习惯:新系统第一版先让模型“看环境”,再让它做少量可控动作。写权限、删除权限、网络权限、凭证访问这些东西,后面单独加。

这就是实战里很容易被忽略的第一层边界:

Agent 的能力不是越大越好。能力要跟验证和回滚一起长。


最小闭环:20 行左右就能看懂

先看核心循环。

下面这段不是完整生产代码,只是把 Agent Loop 的骨架压出来:

async function runAgent(task: string) { const messages = [ { role: "user", content: task }, ]; for (let step = 0; step < 8; step++) { const response = await model.create({ messages, tools, }); messages.push({ role: "assistant", content: response.content, }); if (!response.toolCall) { return response.text; } const observation = await runTool(response.toolCall); messages.push({ role: "tool", content: observation, }); } return "Stopped: step limit reached."; }

它做的事很少:

  1. 把任务和历史交给模型;

  2. 模型返回回答,或者返回工具调用;

  3. 程序执行工具;

  4. 工具结果作为观察写回上下文;

  5. 下一轮继续。

图 1:最小 Agent Loop

很多文章会把这里讲得很神秘。

但把代码摆出来以后,会发现就是一个循环。

如果你觉得这个骨架太简化了,也没关系。生产级 Agent 的实现通常会更长:它们会加上流式响应、事件系统、工具串行/并行调度、工具前后钩子等机制。

但主干没有变,仍然是“调模型 → 判断是否调用工具 → 执行 → 回写上下文 → 继续下一轮”。

我们之前也分析过:《OpenClaw 背后的秘密武器:极简智能体框架 Pi》。

当前agent-loop.ts是 639 行,主干仍然是同一类结构:外层while(true)循环,每轮调模型、检查有没有 toolCall、执行工具、把结果写回上下文、继续。区别在于它多了流式响应、事件系统、工具串行/并行调度、beforeToolCall/afterToolCall钩子这些生产级的东西。但骨架没变。

问题也从这里开始。

这个 loop 能跑,不代表它能用。它没有处理权限,没有限制命令,没有裁剪输出,没有记忆策略,也没有验证完成标准。

也就是说,30 分钟能跑通 Agent。

接下来所有工程问题,都是在回答同一句话:

这个 loop 放进真实世界以后,怎么不乱跑。


工具不是函数列表

最小版本里,我们很容易这样写:

const tools = [ listFiles, readFile, runCommand, ];

这只能说明程序里有三个函数。

对 Agent 来说,还不够。

模型需要知道每个工具能做什么、参数是什么、什么时候该用、失败时会返回什么。程序还要决定参数能不能信,路径能不能读,命令能不能跑,输出要不要裁剪。

所以实际更接近这样:

const tools = [{ name: "read_file", description: "Read a text file inside the current workspace.", inputSchema: { type: "object", properties: { path: { type: "string" }, }, required: ["path"], }, }];

这就是 Function Calling 或 Tools API 解决的问题:让模型用结构化方式表达“我要调用哪个工具、带什么参数”。

现在各家模型的工具接口细节不同,但方向相近:工具名、描述、参数 schema、调用结果,都尽量结构化。

不过这里要补一个很重要的边界。

Function Calling 解决的是“怎么表达工具调用”。

它不会替我们解决这些问题:

  • path是否越过工作区;

  • command是否危险;

  • 工具返回 2MB 日志时要不要全塞回模型;

  • 命令失败后是重试、降级、问人,还是停止;

  • 模型连续调用 8 次还没完成时,系统要不要切断。

Pi 的 coding tools 默认也很克制:ReadBashEditWrite四个核心工具。

第一次看到的时候觉得也太少了。但仔细想想,写软件大多逃不开一条主路径:读代码 → 改代码 → 跑一下看结果。这四个工具刚好覆盖了这条主路径。工具少,系统提示词就短,模型误用工具的概率就低,出了问题也更容易追溯。需要新能力的时候,Pi 更倾向把扩展能力留在系统侧,而不是一开始就把工具列表铺得很大。

说白了就一件事:

工具调用不是把函数暴露给模型,而是把真实世界切成一组可控入口。入口越少,每个入口的边界越容易守住。

图 2:一次工具调用要经过的链路

这也是回头看 Claude Code 时最容易接上的地方。

Claude Code 不是只有“模型 + Bash”。它还围绕 Bash、Read、Edit、WebFetch 等工具做了权限、确认、环境和输出处理。Anthropic 的 Bash tool 文档里也明确提醒:Bash 是直接系统访问能力,需要隔离环境、命令过滤、资源限制和日志。

手搓版用一个run_command就能演示概念。

一旦做成可用工具,边界就要补上。


从提示词解析到 Function Calling:一个容易被跳过的进化

很多教程讲 Agent 时会直接从 Function Calling 开始,好像它天经地义就在那里。

其实第一代“让模型调工具”的做法更原始:在系统提示词里把工具名、参数格式、返回约定全写进去,然后要求模型以 JSON 格式输出调用意图,程序再自己解析 JSON、分发到对应函数。

这套方案能跑,很多早期 demo 都这么做:在系统提示词里写清工具名和参数格式,让模型输出一段约定好的结构化结果,再由程序手动解析、手动分发。

但写过就知道,这里有几个很现实的痛点:

  • 提示词越来越长,工具一多就难维护;

  • 模型输出格式全靠提示词约束,偶尔会跑偏;

  • 解析逻辑要自己写,边界情况不少。

所以 2022 年 ReAct 论文提出了一个关键思路:模型不应该只在文本里猜,它可以通过动作向环境拿新观察(Thought → Action → Observation)。这个范式影响了后面所有 Agent 框架的走向。

2023 年 6 月 13 日,OpenAI 把 Function Calling 放进 Chat Completions API,这条路开始进入主流开发接口:工具名、描述、参数 schema 由 API 层承载,模型用结构化方式表达“我要调什么、带什么参数”,开发者不用再手写 JSON 解析。后来 Anthropic、Google、DeepSeek 都跟进了,接口细节不同,方向一致。

回头看,这个进化过程本身就很能说明一件事:

Agent 工程很少一次设计到位,更常见的是跑起来以后发现边界不够,再一层层补。

提示词解析不稳,就补 Function Calling。工具调用没有边界,就补 schema 校验和权限。模型老是停不下来,就补步数限制和超时。每一层都是从“先能跑”到“跑稳”的过程。

这也是接下来要聊的事。

Loop 会失控,所以要有 Harness

最小循环里,我故意写了一个step < 8

这是一个很土的限制,但很有用。

没有步数限制的 Agent,最容易出现几类问题:

  • 一直调用工具,停不下来;

  • 工具失败了,还假装成功继续往下走;

  • 读了太多文件,把上下文塞脏;

  • 提前回答,结果还没验证;

  • 把中间日志当成最终结论。

这些问题和模型强不强有关,但不全是模型问题。

它们更多发生在运行时。

这也是这半年一直写 Harness 时,我反复想表达的那件事:

Harness 不是给 Agent 套的一层壳。它是把 loop 放进真实工程环境以后,补出来的运行系统。

一个最小 Harness 至少要管这些东西:

  • 最大循环轮数;

  • 最大工具调用次数;

  • 单次工具超时;

  • token 和成本预算;

  • 工具输出裁剪;

  • 错误分类和恢复;

  • 关键动作确认;

  • 日志和回放;

  • 任务完成标准。

Pi 的做法可以拿来参考。它在 agent-loop 里提供了两个钩子:beforeToolCallafterToolCall。前者在工具执行前拦截,可以校验参数、检查权限、直接 block 掉不安全的调用;后者在执行后介入,可以修改返回内容、标记错误、做输出裁剪。这两个钩子不复杂,但把“工具执行前后的控制权”从模型手里拿了回来,交给了工程侧。

图 3:从能跑到可用,中间差的是运行时边界

如果把 Claude Code、Codex、OpenClaw 放到这张图里看,会比单看功能表更清楚。

Claude Code 更偏 Coding Agent:仓库上下文、文件工具、Bash、Todo、Subagent、Compact、权限和验证。

Pi 选了一条很克制的路线:coding tools 默认只有四个核心工具,旁边再放 read-only tools 和扩展系统。这样做的好处是工具面不大,上下文更干净,也更容易审计。

OpenClaw 更偏长期通用 Agent:消息入口、会话、工作区、记忆、插件、网关、安全边界。它底层跑的就是 Pi 的引擎。

三条路线不一样,但拆开看,最小 loop 是共同的。差距发生在 loop 外面:各自补的边界不同,适合的场景也不同。


记忆不是聊天记录越多越好

手搓版第一轮最简单:

messages.push(userMessage); messages.push(assistantMessage); messages.push(toolObservation);

这就是上下文记忆。

短任务够用。

但只要任务稍微长一点,就会遇到几个很现实的问题:

  • • 工具输出越来越多;

  • • 旧错误路径一直留在上下文里;

  • • 对话越长越贵;

  • • 模型开始被无关历史干扰;

  • • 会话一重启,什么都没了。

所以 Memory 不能只理解成“把聊天记录留下来”。

我更愿意把它拆成三层:

层次

保存什么

适合放哪里

当前上下文

当前任务需要的消息、工具结果、文件片段

messages

/ context window

持久事实

项目约定、用户偏好、长期背景

Markdown / DB / profile

过程经验

某类任务以后怎么做、踩坑路径、工作流

Skills / playbooks / procedures

这也是 OpenClaw、Hermes、Clawdbot 这些系统值得拆的地方。

OpenClaw/Clawdbot 走过一条很工程化的路线:把记忆放进工作区文件,让它可读、可改、可审计、可迁移。比如memory/YYYY-MM-DD.md记录流水,MEMORY.md保存更长期的事实;检索时返回片段、路径和行号,而不是把整本记忆塞回去。

Hermes 则把 procedural memory 这层讲得更重:事实和偏好进 memory,任务经验可以沉淀成 skill。它更关心“这类事情以后怎么做”。

这两条路指向同一件事:

记忆系统的目标不是让 Agent 什么都记住,而是让它在合适的时候想起对当前任务有用的东西。

手搓版如果要加记忆,我会先做一个很小的版本:

const memory = { projectRules: readMarkdown("AGENTS.md"), recentSummary: readMarkdown("memory/session-summary.md"), }; const messages = [ { role: "system", content: memory.projectRules }, { role: "system", content: memory.recentSummary }, { role: "user", content: task }, ];

先用 Markdown。

先让人看得懂。

等内容量、召回需求、跨项目复用真的上来,再谈向量库、全文检索、重排和自动整理。

很多系统的问题不在于没有记忆,反倒在于太早把记忆做成黑盒。

我自己的倾向也很明确:先不要把记忆做成黑盒。先用 Markdown、摘要和关键词 检索把最小版本跑通。等记忆量真的上来、简单检索明显不够用时,再加向量化、重 排和自动整理。每一层都应该有明确的痛点驱动。

Agent 工程里很多东西都是这个节奏:先跑通最小版本,碰到瓶颈再加层。


权限决定它能不能长期跑

记忆解决了"Agent 知道什么"的问题。接下来是另一个更敏感的问题:"Agent 能做什么"。

最小 Agent 里最危险的工具,一般是run_command

一旦把 Bash 给模型,能力一下子变大。

它可以列目录、读文件、跑测试、调用 CLI。

也可能误删文件、泄露环境变量、访问外部网络、跑长时间命令。

所以我很少把“给模型一个 Bash”理解成酷炫能力。

它更像一把很锋利的刀。

好用,但要有刀鞘。

Claude Code 的权限系统就是这个方向:哪些工具调用直接允许,哪些要询问,哪些禁止。Anthropic 后来写 Claude Code auto mode 时也讲过一个很实际的矛盾:每次写文件和跑命令都问用户,安全,但会带来 approval fatigue;完全不问,又容易放大风险。

他们没有简单走向“以后都不问”,而是用分类器在动作执行前识别哪些可以放行、哪些需要拦截。文章里也给了边界:在真实 overeager actions 数据集上,完整 pipeline 仍有 17% 的 false-negative rate。

这个数字很有价值。

也就是说,自动化权限系统可以减少打扰,但不能把高风险动作的人类确认完全拿掉。

OpenClaw 的经验也类似。通用 Agent 一旦接入聊天入口、插件、工作区和本地工具,网关和权限边界就不再是“安全附录”,而是主路径的一部分。

所以手搓版到了权限层,我会先切四档:

权限层

例子

默认策略

只读

list_files

read_file

可直接执行

安全执行

npm test

pytestgo test

白名单

写操作

write_file

apply_patch

需要确认

高风险动作

删除、网络、凭证、系统目录

默认禁止

这张表不复杂。

但它决定了 Agent 是一个可控工具,还是一个事故放大器。


验证比“完成了”更重要

权限管住了 Agent 能做什么。但还有一个问题:Agent 说“做完了”,你怎么知道它真的做完了?

Agent 最容易让人误判的地方,就是它很会写“完成了”。

但工程里,完成不是语气,是证据。

手搓版如果只是最后返回:

任务完成。

这个回答没有太大价值。

至少要能回答:

  • 读了哪些文件;

  • 调了哪些工具;

  • 改了什么;

  • 跑了什么测试;

  • 哪些检查通过;

  • 哪些风险还没处理。

对 Coding Agent 来说,最小验证通常包括:

  • 相关测试通过;

  • 类型检查通过;

  • lint 或格式化通过;

  • diff 能对应用户任务;

  • 失败时有错误摘要和下一步建议;

  • 高风险改动有人确认。

这也是我看 Claude Code、Codex、OpenClaw 这些系统时最在意的部分。

模型越来越强以后,“能写出一段代码”不再稀奇。

更稀缺的是,它能不能把上下文、工具、权限、测试、日志和恢复接成一个闭环。

没有验证,Agent 只是很会解释。

有了验证,它才开始接近一个能协作的工程工具。

这也是我自己踩过坑以后的一个体会。传统 CI/CD 跑失败了,你看日志就知道哪行报错。Agent 跑“成功”了,你反而要更警惕,因为模型太擅长用自信的语气说“搞定了”,但它说的“搞定”和工程意义上的“搞定”经常不是一回事。


回头看 Claude Code:它把最小循环做厚了

到这里,回头看 Claude Code 就会顺很多。

之前拆 Pi 源码的时候就有这个感受:它的agent-loop.ts虽然已经是 600 多行的生产代码,但主干和我们手搓版很接近。Claude Code 也是如此,核心 loop 没有什么神秘的新机制。

最小手搓版是:

Model -> Tool -> Observation -> Loop

Claude Code 更像是:

Model -> Repo Context -> Tool System -> Permission -> Bash / Read / Edit -> Todo / Subagent -> Compact / Memory -> Validation -> Loop

它没有把 Agent Loop 变成玄学。

它做的是把一个朴素的 loop 放进真实软件工程环境里。

这也是为什么同一个模型,在聊天框和在 Claude Code 里的体感完全不同。

聊天框里,模型主要在生成文本。你问它“帮我改个 bug”,它只能说“你可以试试这样改”。

Claude Code 里,模型进入了一个工作台:它能看仓库,能读文件,能跑命令,能记录 Todo,能把支线任务交给 subagent,能在上下文变重时 compact,也能在权限系统下行动。它不是在“说”怎么改 bug,而是真的在改。

这背后对应的就是 Harness,也就是模型外面的运行系统。

把前面的内容压成一句话:

30 分钟手搓 Agent,看见的是 loop;Pi、Claude Code、OpenClaw 补上的,是 loop 在真实世界里工作所需要的边界。

http://www.jsqmd.com/news/712510/

相关文章:

  • StructBERT中文情感三分类教程:结果JSON字段含义逐项解读
  • Phi-3-mini-4k-instruct-gguf集成指南:在VSCode中搭建智能开发环境
  • 平波电抗器柔性直流输电线路保护与故障定位实现【附源码】
  • 机器人模仿学习中的动作空间设计与优化
  • 如何学会ECharts
  • 2026年体验了一把品牌服务,原本以为会很套路,结果让我改观了
  • SWAT 模型源码解析与改进示例:不止会用,更要懂原理
  • Git 完整教程
  • 程序员不断学习,具体应该怎么做
  • 振动信号驱动万能式断路器智能故障检测系统【附代码】
  • 2026手机网校搭建平台推荐!助力教培机构数字化教育
  • ARM微控制器引脚配置与交叉开关架构实战指南
  • 2026上半年多商户小程序权威服务商盘点:哪家更适配你的电商需求
  • 前端测试:Cypress 集成测试最佳实践
  • 多进程不只是绕过 GIL:从 Python 性能优化到进程级隔离的工程实战
  • NVIDIA Cosmos Policy:机器人控制策略的模块化与仿真训练实践
  • 守护服务器安全|OpenSSH CVE-2024-6387 漏洞深度剖析 + 实操修复指南
  • 位运算基础与进阶
  • 五国朋友齐聚这里周五静安英语角
  • 国产替代崛起,白酒崩!
  • 临时停车系统厂家深度测评:全场景适配运维实力综合解析
  • egergergeeert企业落地实践:用低显存模式支撑日常插画草图迭代需求
  • 笔记软件换了一个又一个还是不满意?Trilium用下来解决了我的知识管理焦虑
  • DeepSeek V4 突然发布,DeepSeek-V4 技术报告深度解读
  • WeDLM-7B-Base镜像免配置:预置webui.py+supervisor.conf开箱即用
  • Rust生命周期:杜绝悬垂指针的终极指南
  • C#怎么设置JWT身份认证_C#如何生成并验证Token令牌【实战】
  • 前端微前端:Webpack 5 Module Federation 深度解析
  • 人力资源管理——解读全面掌握OKR:目标设定与持续绩效管理的实践指南【附全文阅读】——文末附下载链接
  • HTTP (XSS前简单了解)