手写一个最小版 Claude Code:从任务拆解到 Agent Loop 转起来

写在前面

你说"帮我建个 react+vite 的 todolist",Claude Code 自己就把项目搭好、代码写好、npm run dev 跑起来。

看起来像魔法,拆开看就是一句话:LLM + Tool(fs + cli)

今天咱们照着这个思路,把第一个能跑的编程 Agent 从任务规划、工作流编排、Message 协议、到 while 循环转起来,全流程串一遍。

Demo 拆任务:一个三步走的规划

Agent 接到"创建 react+vite todolist"这个任务,第一件事不是写代码,是规划

js

arduino 复制代码
// 任务:创建一个 react+vite 的 todolist

LLM 拆成三步,每步对应一个工具:

步骤 干啥 需要的 Tool
1 vite 创建项目脚手架 写入文件 Tool(落地 package.json、vite.config)
2 编写组件、样式、逻辑 写入文件 Tool(落地 App.jsx、TodoList.jsx)
3 装依赖 + 跑起来 CLI 命令 Tool(npm install + npm run dev

这叫 planning ------LLM 自己拆任务、自己决定每一步用哪个工具。一个最小版 Claude Code 的工具盘就是 fs(读写文件)+ cli(执行命令),两个工具撑起整个编码 Agent。

LangChain:LLM 界的工作流编排框架

LangChain 比 OpenAI SDK 还早出生,定位很清楚:LLM 应用开发框架

它解决一个核心痛点------LLM 厂商太多,今天用 OpenAI、明天切 DeepSeek、后天换 Qwen,难道每换一家就重写一遍?

LangChain 用一套统一抽象把它们接进来,@langchain/openai 只是一个适配器,换厂商改一行配置就完事。

工作流的编排像搭积木,也像 Coze 里节点之间的连线:

rust 复制代码
ChatOpenAI(模型)
   ↓
tools(声明工具,async fn + zod schema)
   ↓
bindTools(把工具绑到模型上,切到"工具模式")
   ↓
invoke(喂 messages,跑起来)

bindTools 这一步是关键开关------绑了工具,LLM 才知道"这事我能动手",否则它只会给你回一段伪代码。

4 种 Message:Agent 的对话协议

Agent 跟 LLM 通信靠 messages 数组,LangChain 给消息分了四种派生类:

Message 类型 谁说的 装什么 关键字段
SystemMessage 开发者设定 AI 是谁、能干啥、行为规范 系统提示词
HumanMessage 用户 任务指令 用户输入
AIMessage LLM 推理结果 + 工具调用意图 tool_calls
ToolMessage 你的代码 工具执行结果 tool_call_id

tool_call_id 是这一轮的身份证。LLM 一次可能调多个工具,每个 tool_calls 带 id,工具跑完回传 ToolMessage 时必须带上同一个 id------LLM 靠它对账"这个结果是我刚才哪个调用产生的"。

四种 Message 在数组里按时间顺序排好,就是 Agent 的完整对话上下文。

原生 OpenAI vs LangChain:返回差异

原生 OpenAI SDK 返回的工具调用塞在 additional_kwargs.tools 里,裸奔状态,你自己抠。

LangChain 的 invoke 原样保留了这些信息,还贴心地把 tool_calls 提到顶层,方便你直接遍历。

维度 原生 OpenAI LangChain
工具调用位置 additional_kwargs.tools 顶层 tool_calls + 保留 kwargs
工程便捷性 自己解析 框架帮你准备好
可读性 一般
切厂商成本 重写 改一行配置

这就是"框架"的价值------不是它做了你做不了的事,是它把要做的事做得更顺手、更可读、更可维护。

最简 Agent Loop:while 循环转起来

前面都是零件,真正让 Agent 活过来的是这个循环

js

javascript 复制代码
let messages = [
  new SystemMessage('你是代码助手,可用工具:write_file、run_cli'),
  new HumanMessage('帮我创建一个 react+vite 的 todolist'),
];

// 最简单的 loop
while (true) {
  const response = await modelWithTools.invoke(messages);
  messages.push(response);

  // 没有 tool_calls → LLM 已经拿到结果,准备最终回答
  if (!response.tool_calls?.length) {
    break;  // 循环退出,任务完成
  }

  // 有 tool_calls → 并行执行所有工具,结果回喂
  const toolResults = await Promise.all(
    response.tool_calls.map(call => tools[call.name].invoke(call.args))
  );
  toolResults.forEach((result, i) => {
    messages.push(new ToolMessage({
      tool_call_id: response.tool_calls[i].id,
      content: result,
    }));
  });
}

逐段拆解:

段落 干啥 为什么这么写
while(true) 持续运转 Agent 就是循环,不到完成不停
invoke + push 每轮都把 AI 回复塞回数组 LLM stateless,靠 messages 数组维持上下文
if (!tool_calls) 没有工具调用就退出 LLM 觉得"够了"才会直接给答案
Promise.all(map) 并行跑所有工具 一轮可能调多个工具,串行太慢
ToolMessage + tool_call_id 结果回喂 让 LLM 对上号

这就是笔记里说的"最简单的 loop 有工具调用"------有就继续转,没有就最后一次 invoke 拿结果

async/Promise 在 Agent 里的位置

整个 Agent 几乎全是 async 函数,因为每一步都在等------等 LLM、等工具、等文件 IO。

几个要点记一下:

特性 在 Agent 里的用法
async 函数 = Promise 实例 整个 main() 是 async,return 的值就是 resolve 的值
await 等 LLM 回复、等工具跑完
Promise.all 一轮多工具并行,谁也别等谁
Array.find/map 在 tools 数组里按 name 找工具、map 出工具结果
try/catch 工具可能挂(文件不存在、CLI 报错),必须兜底

特别提一句 tools[call.name] 这种写法------把工具做成一个 name 到函数的 map,LLM 吐 call.name,你直接查表执行。比 if/else 一长串判断干净多了。

5 个踩坑提醒

1. 忘了把 response push 回 messages。 LLM 是 stateless 的,上一轮它说了啥它自己不知道。不把 AI 回复塞回数组,下一轮它就"失忆",循环直接乱套。

2. ToolMessage 不带 tool_call_id。 多工具并行时,LLM 靠 id 对账。丢了 id,结果成了无头尸,LLM 不知道这个结果对应哪次调用。

3. while 没有退出保护。 万一 LLM 一直吐 tool_calls,循环永远不退。加个最大轮次计数器,到上限强制 break。

4. 工具执行不 try/catch。 文件不存在、CLI 报错,Promise 直接 reject,整个 Agent 崩。每个工具内部包 try/catch,把错误信息当结果回喂给 LLM,让它自己决定重试还是换思路。

5. async 函数当同步用。 const result = someAsyncTool(args) 不 await,拿到的是 Promise 不是数据。Agent 里到处是坑,养成"async 必 await"的肌肉记忆。

写在最后

一个能跑的编程 Agent,核心就这些:LLM 拆任务 → LangChain 编排工具 → 4 种 Message 维持上下文 → while 循环转起来

零件不多,难的是把它们接得稳、转得稳。Claude Code 看着强大,拆到底也是这套结构,只是工具更全、工程化更狠。

相关推荐
给个offer养家糊口1 小时前
抽离 elpis npm 包
前端
默_笙1 小时前
🙃 我让爬虫终于看到了我的网站,后端同事说"这也行?"(下):App Router 全栈实战
前端·javascript
葡萄城技术团队1 小时前
一个单元格放置两个日期选择器:用 SpreadJS CellButtons 录入日期范围
前端
马可家的菠萝2 小时前
自动保存已经有了,为什么笔记软件还需要“历史版本”?
前端·后端·架构
半个落月2 小时前
从 "use client" 到 Route Handler:用 Todos 理解 Next.js 水合与全栈请求
前端·react.js·next.js
qq_452396233 小时前
第七篇:《大型前端项目的模块化与目录结构设计》
前端
你脑门上的脚印3 小时前
Vue 项目从零实现语音转文字、文字转语音功能(完整可用 + 踩坑指南)
前端·vue.js
paopaokaka_luck3 小时前
基于springboot3+vue3的车间生产管理系统(Echarts图形化分析、BI报表)
java·前端·spring boot·学习·echarts
程序员鱼皮3 小时前
3 大 DeepSeek Harness 进阶玩法,招多个大肥鱼帮我干活!
前端·后端·ai编程