跳到主要内容

最小 Agent 代码实现示例

如果说 从 0 到 1 做一个最小 Agent Demo 解决的是:

第一个 Agent 应该做成什么样

那这篇文档要解决的就是另一个非常实际的问题:

一个最小 Agent,代码上到底应该长什么样?

很多人在学 AI Agent 时,已经理解了:

  • 要有目标
  • 要有工具
  • 要有状态
  • 要有循环

但一落到代码层,还是会卡在这些地方:

  • tool 到底该怎么定义
  • state 应该存什么
  • 循环到底怎么停
  • 每一步日志记什么
  • 第一个版本应该写到多复杂

所以这篇文档不讲大而全框架,而是只做一件事:

给你一个可以真正照着写出来的最小 Agent 代码骨架。

这里我会用 TypeScript / Node.js 风格来写伪实现,因为它足够直观,也比较适合你后面自己改成真实项目代码。

1. 先定目标:不要一开始就写“万能 Agent”

最小代码示例最重要的原则不是“功能多”,而是:

边界清楚。

所以我们先假设一个非常适合起步的场景:

文档调研助手

用户会说:

帮我判断某个 AI 框架适不适合做内部知识助手。

这个场景很好,因为它天然要求系统去做几件事:

  • 理解目标
  • 决定查什么
  • 调一个搜索或读取工具
  • 根据结果继续或停止
  • 最后输出结论

这已经足够体现 Agent 的味道。

2. 先定义最小系统结构

先不要急着写循环,可以先把系统拆成 5 块:

  1. model
  2. tools
  3. state
  4. agent loop
  5. final synthesis

写成最小结构就是:

User Goal -> Decide -> Call Tool -> Observe -> Update State -> Continue or Stop -> Final Answer

这条线就是你第一版代码要表达的全部核心。

3. 最小状态应该长什么样

很多人第一版 state 写得太大,结果还没开始跑,结构就已经失控了。

最小版建议只保留下面这些:

type AgentState = {
goal: string;
currentStep: number;
maxSteps: number;
notes: string[];
evidence: string[];
toolCalls: ToolCallRecord[];
pendingQuestions: string[];
done: boolean;
};

type ToolCallRecord = {
toolName: string;
args: Record<string, unknown>;
summary: string;
};

这套结构已经足够支持第一版系统:

  • goal:当前任务目标
  • currentStep / maxSteps:避免无限循环
  • notes:存中间结论草稿
  • evidence:存证据
  • toolCalls:记录调用轨迹
  • pendingQuestions:记录还没解决的问题
  • done:控制退出

第一版最重要的不是“全面”,而是:

你能清楚看到系统每一步知道了什么、做了什么、还缺什么。

4. 工具定义要尽量傻一点

Tool-Using Agent 第一版最怕的,不是工具太少,而是工具太“万能”。

比如一开始就写一个:

search_everything_and_return_anything

这种工具基本一定会把系统做乱。

更好的方式是:

type ToolDefinition = {
name: string;
description: string;
run: (args: Record<string, unknown>) => Promise<string>;
};

const searchDocsTool: ToolDefinition = {
name: "search_docs",
description: "Searches documentation or articles by topic keyword",
async run(args) {
const query = String(args.query ?? "");
return `Search results for: ${query}`;
},
};

const readDocTool: ToolDefinition = {
name: "read_doc",
description: "Reads a selected document or page content",
async run(args) {
const source = String(args.source ?? "");
return `Document content from: ${source}`;
},
};

这类工具有几个好处:

  • 职责单一
  • 参数容易理解
  • 返回值容易记录
  • 出错时更容易排查

第一版系统里,工具越“笨”,整体越稳。

5. 决策层先不要追求太聪明

很多人写 Agent 时,一开始就想做:

  • 自动规划
  • 自动反思
  • 自动重排任务
  • 动态工具路由

这些以后都可以加,但第一版更重要的是:

先让系统稳定地做出“下一步是搜索、阅读还是结束”这种小决策。

最小决策结果可以先长这样:

type AgentDecision =
| { type: "tool"; toolName: string; args: Record<string, unknown>; reason: string }
| { type: "finish"; reason: string };

也就是说,第一版只允许两种事:

  1. 调一个工具
  2. 结束任务

这会让循环非常清楚。

6. 一个最小 Agent Loop 长什么样

下面这个例子不是生产代码,但它已经足够接近真实实现:

type ToolDefinition = {
name: string;
description: string;
run: (args: Record<string, unknown>) => Promise<string>;
};

type ToolCallRecord = {
toolName: string;
args: Record<string, unknown>;
summary: string;
};

type AgentState = {
goal: string;
currentStep: number;
maxSteps: number;
notes: string[];
evidence: string[];
toolCalls: ToolCallRecord[];
pendingQuestions: string[];
done: boolean;
};

type AgentDecision =
| { type: "tool"; toolName: string; args: Record<string, unknown>; reason: string }
| { type: "finish"; reason: string };

const tools: ToolDefinition[] = [
{
name: "search_docs",
description: "Searches docs by topic",
async run(args) {
return `Search results for ${String(args.query ?? "")}`;
},
},
{
name: "read_doc",
description: "Reads a selected document",
async run(args) {
return `Document content from ${String(args.source ?? "")}`;
},
},
];

function getToolByName(name: string) {
return tools.find((tool) => tool.name === name);
}

async function decideNextAction(state: AgentState): Promise<AgentDecision> {
if (state.currentStep === 0) {
return {
type: "tool",
toolName: "search_docs",
args: { query: state.goal },
reason: "Need broad material first",
};
}

if (state.evidence.length < 2 && state.toolCalls.length > 0) {
return {
type: "tool",
toolName: "read_doc",
args: { source: "top-search-result" },
reason: "Need one level deeper evidence",
};
}

return {
type: "finish",
reason: "Enough evidence collected for a first conclusion",
};
}

function updateStateWithToolResult(
state: AgentState,
decision: Extract<AgentDecision, { type: "tool" }>,
result: string
) {
state.toolCalls.push({
toolName: decision.toolName,
args: decision.args,
summary: result.slice(0, 120),
});

state.evidence.push(result);
state.notes.push(`Step ${state.currentStep + 1}: ${decision.reason}`);
}

function printStepLog(
state: AgentState,
message: string,
extra?: Record<string, unknown>
) {
console.log(
JSON.stringify({
step: state.currentStep,
goal: state.goal,
message,
...extra,
})
);
}

async function runAgent(goal: string) {
const state: AgentState = {
goal,
currentStep: 0,
maxSteps: 4,
notes: [],
evidence: [],
toolCalls: [],
pendingQuestions: [],
done: false,
};

while (!state.done && state.currentStep < state.maxSteps) {
const decision = await decideNextAction(state);

printStepLog(state, "decision_made", {
decisionType: decision.type,
reason: decision.reason,
});

if (decision.type === "finish") {
state.done = true;
break;
}

const tool = getToolByName(decision.toolName);
if (!tool) {
throw new Error(`Unknown tool: ${decision.toolName}`);
}

const result = await tool.run(decision.args);

printStepLog(state, "tool_finished", {
toolName: tool.name,
args: decision.args,
resultPreview: result.slice(0, 120),
});

updateStateWithToolResult(state, decision, result);
state.currentStep += 1;
}

return {
summary: buildFinalAnswer(state),
state,
};
}

function buildFinalAnswer(state: AgentState) {
return [
`Goal: ${state.goal}`,
`Evidence count: ${state.evidence.length}`,
`Conclusion: This is a first-pass recommendation based on collected evidence.`,
`Notes: ${state.notes.join(" | ")}`,
].join("\n");
}

这个例子里最关键的不是具体语法,而是你要看懂下面这几个边界:

  • decideNextAction 只负责决定下一步
  • tool.run 只负责做动作
  • updateStateWithToolResult 只负责更新状态
  • buildFinalAnswer 只负责收束输出

这就是最小 Agent 骨架。

7. 为什么这个骨架适合起步

因为它解决了新手最常见的几个问题:

7.1 不会把所有逻辑揉成一坨

你能明确分清:

  • 决策
  • 执行
  • 状态
  • 输出

7.2 不会太早陷入框架绑定

你完全可以先自己写明白这个闭环,再决定后面要不要上:

  • LangGraph
  • SDK 型 agent framework
  • workflow engine

先把骨架看懂,比先学框架更重要。

7.3 足够支持调试

因为你已经有:

  • 每一步决策
  • 工具参数
  • 工具结果摘要
  • 中间状态

这意味着它不是“能跑就算了”,而是开始具备最基础的可观测性。

8. 第一版最值得补的 3 个增强

如果这个最小骨架已经跑起来,我建议下一步只加下面 3 样东西:

  1. 真实模型决策decideNextAction 从硬编码逻辑,替换成模型输出结构化决策。

  2. 真实工具返回 把搜索 / 阅读工具接到真实数据源上。

  3. 最小评估样本 准备 5 到 10 个固定任务,反复跑,看:

  • 是否完成目标
  • 是否选对工具
  • 是否步骤过多

不要一开始就补:

  • 多 Agent
  • 长期记忆
  • 复杂权限系统
  • 自动反思链

这些都会把学习曲线突然拉陡。

9. 一个更接近真实产品的最小升级版

如果你想让这套骨架更接近真实产品,而又不一下变得太复杂,可以只补下面几件事:

type AgentRunMetrics = {
totalSteps: number;
totalToolCalls: number;
stopReason: string;
durationMs: number;
};

再配上:

  • 超时保护
  • 最大步数保护
  • 工具异常处理
  • 简单的运行指标

这样你就开始从“会写一个 demo”走向“会写一个能被调试和迭代的小系统”。

10. 学这篇时最应该抓住什么

如果只抓一件事,我最希望你记住的是:

最小 Agent 代码实现,不是先追求聪明,而是先追求边界清楚。

第一版代码只要把下面这条线写明白,就已经很有价值:

目标 -> 决策 -> 工具 -> 观察 -> 状态更新 -> 停止或继续 -> 最终输出

当你能把这条线用代码稳定表达出来,后面再加:

  • 更强模型
  • 更多工具
  • 更复杂状态
  • 更正式的 evals
  • 更完整的 harness

都会自然很多。

11. 一句话总结

最适合学习的第一个 Agent 代码实现,不是功能最全的,而是把目标、决策、工具、状态和日志拆清楚的那一个。