最小 Agent 代码实现示例
如果说 从 0 到 1 做一个最小 Agent Demo 解决的是:
第一个 Agent 应该做成什么样
那这篇文档要解决的就是另一个非常实际的问题:
一个最小 Agent,代码上到底应该长什么样?
很多人在学 AI Agent 时,已经理解了:
- 要有目标
- 要有工具
- 要有状态
- 要有循环
但一落到代码层,还是会 卡在这些地方:
tool到底该怎么定义state应该存什么- 循环到底怎么停
- 每一步日志记什么
- 第一个版本应该写到多复杂
所以这篇文档不讲大而全框架,而是只做一件事:
一份可以直接照着写的最小 Agent 代码骨架。
这里用 TypeScript / Node.js 风格来写伪实现,因为它足够直观,后面改成真实项目代码也方便。
1. 先定目标:不要一开始就写“万能 Agent”
最小代码示例最重要的原则不是“功能多”,而是:
边界清楚。
所以先假设一个适合起步的场景:
文档调研助手
用户会说:
帮我判断某个 AI 框架适不适合做内部知识助手。
这个场景很好,因为它天然要求系统去做几件事:
- 理解目标
- 决定查什么
- 调一个搜索或读取工具
- 根据结果继续或停止
- 最后输出结论
这已经足够体现 Agent 的味道。
2. 先定义最小系统结构
先不要急着写循环,可以先把系统拆成 5 块:
modeltoolsstateagent loopfinal 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 };
第一版只允许两种事:
- 调一个工具
- 结束任务
这会让循环非常清楚。
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 骨架。