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最小 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 默认也很克制Read、Bash、Edit、Write四个核心工具。第一次看到的时候觉得也太少了。但仔细想想写软件大多逃不开一条主路径读代码 → 改代码 → 跑一下看结果。这四个工具刚好覆盖了这条主路径。工具少系统提示词就短模型误用工具的概率就低出了问题也更容易追溯。需要新能力的时候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 里提供了两个钩子beforeToolCall和afterToolCall。前者在工具执行前拦截可以校验参数、检查权限、直接 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、pytest、go 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 - LoopClaude 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看见的是 loopPi、Claude Code、OpenClaw 补上的是 loop 在真实世界里工作所需要的边界。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2561896.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!