上一篇文章里,我用 300 行代码实现了一个能对话、能调用工具的 Agent。它能读写文件、执行命令、搜索代码。这已经算是一个很基础的 agent 的,然后我在前天 300 行的基础上,继续增加了可以执行 shell 命令,为了安全,我还增加了审核机制,比较危险的命令需要人工确认才可以执行。
然后,我就开始继续想,如果一个复杂的任务,需要多步来执行应该怎么办。现在其实我们知道了,很多编码工具都会做一个 TODO,所以,一个真正的助手,应该先想清楚要做什么,再一步步执行。这个已然是一个共识。
想象你要装修房子:
买点油漆刷墙...等等,好像应该先拆旧墙?算了先刷着...
Agent 也应该这样。这就是 Plan-and-Execute 模式。其实严格俩说,我们上一个版本的 agent 叫做 ReAct Agent ,不信反翻回上面的文章看俺他其实可以自主多步调用工具了,这是模型的能力,当模型判断当前任务还没完成,会自动寻找可以完成的路径去做执行。
想清楚后,需求很简单:
为了实现这个,我选择用 jsonl 文件来存储我的会话和 plan。
其直接原因还是因为我看了 Claude Code 的实现,他就是用 jsonl 来存储会话的。
对话历史用什么格式存?
| JSONL |
我选了 JSONL。每行一个 JSON:
{"type":"meta","id":"abc123","title":"创建React项目","created":"..."}{"type":"message","role":"user","content":"帮我创建项目","ts":"..."}{"type":"message","role":"assistant","content":"好的,我来规划...","ts":"..."}{"type":"tool_call","tool_calls":[...],"ts":"..."}为什么?
fs.appendFileSync,不用读取整个文件一开始我把 sessions 放在项目目录下:
agent/├── sessions/ ← 不对!│ └── xxx.jsonl└── index.js这有问题:
正确做法是放在用户目录:
constAGENT_HOME = path.join(os.homedir(), ".agent");constSESSIONS_DIR = path.join(AGENT_HOME, "sessions");// → ~/.agent/sessions/这是 Unix 惯例。npm 用 ~/.npm,git 用 ~/.gitconfig,我们用 ~/.agent。
最初我把 Plan 也存在 JSONL 里:
{"type":"plan","steps":[{"id":1,"task":"创建项目","status":"pending"}]}{"type":"plan_update","stepId":1,"status":"done"}{"type":"plan_update","stepId":2,"status":"done"}问题:
更好的做法:Plan 单独存成 JSON 文件,直接覆盖更新:
~/.agent/sessions/├── 2024-01-15_abc123.jsonl # 对话历史(追加)└── 2024-01-15_abc123.plan.json # 计划(覆盖)// plan.json - 直接读写,状态实时{"steps":[{"id":1,"task":"创建项目","status":"done"},{"id":2,"task":"安装依赖","status":"pending"}],"updated":"2024-01-15T10:05:00Z"}原则:追加型数据用 JSONL,状态型数据用 JSON。
给 Agent 加了三个新工具:
// 1. 创建计划createPlan({ steps: ["步骤1", "步骤2", ...] })// 2. 更新状态updatePlanStep({ stepId: 1, status: "done", result: "项目创建成功" })// 3. 查看计划getPlan()工具的 Schema 长这样:
exportconst createPlan = {schema: {type: "function",function: {name: "createPlan",description: "创建执行计划。复杂任务时先规划再执行。",parameters: {type: "object",properties: {steps: {type: "array",items: { type: "string" },description: "计划的步骤列表", }, },required: ["steps"], }, }, },execute: async ({ steps }, context) => { context.createPlan(steps);return"📋 已创建执行计划:\n" + steps.map((s, i) =>` ${i+1}. [ ] ${s}`).join("\n"); },};关键在 description:告诉 AI 什么时候该用这个工具。



整个过程完成下来,我们这个800 行的 agent 就可以创建一个完整的工程,并且跑起来项目了。

最神奇的是:我没写任何控制逻辑。
AI 自己知道要先创建计划,然后一步步执行,完成一步就更新状态。这就是工具描述的力量。
没有 Plan:AI 是个黑盒,你不知道它要做什么,无法自我驱动有了 Plan:你能看到它的计划,能中断,能调整
description: "创建执行计划。复杂任务时先规划再执行。"这一句话,决定了 AI 什么时候会用这个工具。Prompt Engineering 不只是写系统提示词,工具描述同样关键。
现在的 Agent 还缺什么?
但这些都是锦上添花。核心已经有了:一个能记忆、能规划、能执行的 Agent。
需要源码?建议还是自己实现把,不实现一次,真的你很难发现这种乐趣~