前端开发者做 Agent:别写成一次请求,用 5 步受控循环防止 AI 乱跑
作者:前端转 AI 深度实践者
【省流助手/核心观点】:Agent 不是“调用一次大模型”就结束,也不是让模型无限自由发挥。一个最小 Agent Loop 至少要经历 5 步:接收用户输入、模型决定是否调用工具、程序执行工具、工具结果写回上下文、模型生成最终回答。真正可靠的 Agent Loop 必须受控:有
maxSteps、有工具边界、有结构化输出、有错误处理、有 trace 记录。对前端开发者来说,它很像一个带状态、带副作用、带退出条件的事件循环。
很多人第一次做 Agent,会把它想得过于神秘。
仿佛只要 Prompt 写成:
你是一个自主智能体,请一步一步完成用户任务。
模型就会自己查资料、自己调接口、自己整理结论、自己处理异常。
这个想象很美,但工程系统不能靠想象跑。
真正的 Agent,不是让模型无限自由发挥。
真正的 Agent,是让模型和工具在程序控制下协作。
这一层协作,叫 Agent Loop。
1. 痛点:一次调用只能聊天,不能稳定办事
前两篇我们讲了 Tool Calling 和工具 Schema。
模型不应该编真实世界的信息,而应该提出工具调用意图。
比如用户问:
帮我查一下订单 A1001 到哪了,并告诉我什么时候能收到。
模型可以输出:
{
"type": "tool_call",
"toolName": "getOrderStatus",
"args": {
"orderId": "A1001"
}
}
程序执行工具,得到:
{
"status": "shipping",
"eta": "2026-05-03"
}
问题来了:这就结束了吗?
对程序来说,工具结果已经拿到了。
但对用户来说,他想看的不是 JSON,而是一句能读懂的话:
订单 A1001 当前运输中,预计 2026-05-03 送达。
所以完整流程应该是:
用户提问
-> 模型提出工具调用
-> 程序执行工具
-> 工具结果写回上下文
-> 模型组织最终回答
这就是最小 Agent Loop。
2. 错误做法:把 Agent 写成一次模型请求
很多早期实现会这样写:
async function askAgent(question: string) {
const prompt = `
你是一个智能助手,请完成用户任务。
用户问题:${question}
`;
return llm.chat(prompt);
}
这段代码的问题是:它没有工具执行,也没有状态回写。
模型只能生成一段回答。
如果答案涉及订单、库存、权限、合同、退款金额,它很可能是在“猜”。
另一种常见错误,是只执行一次工具,然后直接把工具 JSON 返回给用户:
async function askOrderOnce(orderId: string) {
const result = await getOrderStatus({ orderId });
return JSON.stringify(result);
}
这看起来接了工具,但还不是 Agent Loop。
因为它缺了最后一步:让模型基于工具结果组织面向用户的回答。
也缺了更重要的一点:当模型还需要第二个工具时,系统没有继续循环的能力。
3. 正确做法:把模型输出设计成两种类型
在普通聊天里,模型输出通常就是一段文本。
但在 Agent 里,模型输出至少有两种可能:
type ModelOutput =
| {
type: "tool_call";
toolName: string;
args: Record<string, unknown>;
}
| {
type: "final_answer";
content: string;
};
第一种是工具调用:
{
"type": "tool_call",
"toolName": "getOrderStatus",
"args": {
"orderId": "A1001"
}
}
意思是:现在信息不够,需要程序帮我调用工具。
第二种是最终回答:
{
"type": "final_answer",
"content": "订单 A1001 当前运输中,预计 2026-05-03 送达。"
}
意思是:我已经可以回答用户了。
Agent Loop 的核心判断就一句话:
如果是 tool_call,就执行工具,然后继续。
如果是 final_answer,就返回答案,然后结束。
复杂 Agent 会加入规划、记忆、多工具、多轮任务,但地基仍然是这个判断。
4. 先用 mockModel 练骨架,别一上来接真实 API
很多人一上来就接真实模型,结果调试时一团乱:
- 是 Prompt 不清楚?
- 是模型没有按格式返回?
- 是工具调度器有 bug?
- 是参数校验拦错了?
- 是 API 调用失败?
- 是网络或环境变量问题?
更稳的做法是:先用 mockModel。
就像前端开发时,后端接口没好,你会先 mock 数据,把页面状态和交互跑通。
Agent 也一样。
type Message =
| {
role: "user";
content: string;
}
| {
role: "tool";
toolName: string;
content: unknown;
};
function mockModel(messages: Message[]): ModelOutput {
const lastMessage = messages[messages.length - 1];
if (lastMessage.role === "user") {
if (lastMessage.content.includes("A1001")) {
return {
type: "tool_call",
toolName: "getOrderStatus",
args: { orderId: "A1001" }
};
}
return {
type: "final_answer",
content: "我暂时只能处理订单 A1001 的查询。"
};
}
if (lastMessage.role === "tool") {
const result = lastMessage.content as {
status?: string;
eta?: string;
};
return {
type: "final_answer",
content: `订单当前状态为 ${result.status},预计 ${result.eta} 送达。`
};
}
return {
type: "final_answer",
content: "无法处理当前请求。"
};
}
它不聪明,但它稳定。
稳定的好处是,你可以先验证 Agent Loop 的工程结构:
-
messages是否维护正确。 - 工具调用是否被执行。
- 工具结果是否写回上下文。
- 最终回答是否能结束循环。
- 错误路径是否会返回。
等骨架跑稳了,再接真实模型,问题会清楚很多。
5. messages 是 Agent 的短期记忆
Agent Loop 里最重要的数据结构之一是 messages。
它记录了整个运行过程:
const messages: Message[] = [
{
role: "user",
content: "帮我查订单 A1001"
},
{
role: "tool",
toolName: "getOrderStatus",
content: {
status: "shipping",
eta: "2026-05-03"
}
}
];
为什么工具结果要写回 messages?
因为模型下一轮需要看到它。
如果工具执行完了,但你没有把结果放回上下文,模型就像刚查完资料却失忆:
工具查到了,但模型不知道查到了什么。
这类 bug 在 Agent 开发里非常常见。
记住一个规则:
工具执行不是终点,工具结果必须回到上下文。
6. 一个最小 Agent Loop 长这样
先准备一个工具和调度器:
type ToolResult =
| {
ok: true;
toolName: string;
data: unknown;
}
| {
ok: false;
toolName?: string;
error: string;
};
async function getOrderStatus(args: Record<string, unknown>) {
if (args.orderId !== "A1001") {
return { status: "not_found" };
}
return {
status: "shipping",
eta: "2026-05-03"
};
}
async function runTool(output: Extract<ModelOutput, { type: "tool_call" }>) {
if (output.toolName !== "getOrderStatus") {
return {
ok: false,
toolName: output.toolName,
error: `未知工具:${output.toolName}`
} satisfies ToolResult;
}
const data = await getOrderStatus(output.args);
return {
ok: true,
toolName: output.toolName,
data
} satisfies ToolResult;
}
然后写 Agent Loop:
type AgentRunResult =
| {
ok: true;
answer: string;
steps: number;
messages: Message[];
}
| {
ok: false;
error: string;
steps: number;
messages: Message[];
};
async function runAgentLoop(
userInput: string,
maxSteps = 3
): Promise<AgentRunResult> {
const messages: Message[] = [
{
role: "user",
content: userInput
}
];
for (let step = 1; step <= maxSteps; step++) {
const output = mockModel(messages);
if (output.type === "final_answer") {
return {
ok: true,
answer: output.content,
steps: step,
messages
};
}
if (output.type === "tool_call") {
const toolResult = await runTool(output);
messages.push({
role: "tool",
toolName: output.toolName,
content: toolResult.ok ? toolResult.data : toolResult
});
continue;
}
return {
ok: false,
error: "模型输出了未知类型",
steps: step,
messages
};
}
return {
ok: false,
error: "超过最大步骤数,Agent 已停止",
steps: maxSteps,
messages
};
}
这段代码已经包含 Agent 的核心骨架:
- 有用户输入。
- 有模型决策。
- 有工具执行。
- 有上下文更新。
- 有最终回答。
- 有最大步数。
- 有异常退出。
复杂 Agent 往后扩展,基本都是在这个骨架上继续加能力。
7. maxSteps 是 Agent 的安全绳
为什么一定要有 maxSteps?
因为模型可能会反复调用工具。
比如一个异常模型每次都返回:
{
"type": "tool_call",
"toolName": "searchPolicy",
"args": {
"keyword": "报销"
}
}
如果你没有最大步数,Agent 就会一直查,一直查,一直查。
这不是智能,这是迷路。
maxSteps 就像安全绳:
最多跑 3 步,跑不完就停下来,把错误返回。
玩具 demo 假设模型总是乖。
工程系统默认任何环节都可能出错。
8. 前端页面怎么展示 Agent Loop?
如果你只展示最终回答,排查 Agent 问题会很痛苦。
建议至少在开发环境或内部后台展示步骤 trace。
type AgentTraceStep = {
step: number;
modelOutput: ModelOutput;
toolResult?: ToolResult;
};
Agent Loop 运行时记录 trace:
async function runAgentLoopWithTrace(userInput: string, maxSteps = 3) {
const messages: Message[] = [{ role: "user", content: userInput }];
const trace: AgentTraceStep[] = [];
for (let step = 1; step <= maxSteps; step++) {
const output = mockModel(messages);
const traceStep: AgentTraceStep = {
step,
modelOutput: output
};
if (output.type === "final_answer") {
trace.push(traceStep);
return {
ok: true,
answer: output.content,
messages,
trace
};
}
const toolResult = await runTool(output);
traceStep.toolResult = toolResult;
trace.push(traceStep);
messages.push({
role: "tool",
toolName: output.toolName,
content: toolResult.ok ? toolResult.data : toolResult
});
}
return {
ok: false,
error: "超过最大步骤数,Agent 已停止",
messages,
trace
};
}
前端可以把 trace 做成折叠面板:
function AgentTracePanel({ trace }: { trace: AgentTraceStep[] }) {
return (
<details>
<summary>Agent 执行过程</summary>
{trace.map((item) => (
<section key={item.step}>
<h4>Step {item.step}</h4>
<pre>{JSON.stringify(item.modelOutput, null, 2)}</pre>
{item.toolResult && (
<pre>{JSON.stringify(item.toolResult, null, 2)}</pre>
)}
</section>
))}
</details>
);
}
这不是炫技,而是为了让 Agent 出问题时能定位:
- 模型哪一步选错了工具?
- 参数是哪一步变错的?
- 工具有没有执行成功?
- 工具结果有没有回到上下文?
- 为什么没有生成最终回答?
9. Agent Loop 不是无限自治,而是受控协作
很多人喜欢把 Agent 描述成“自主智能体”。
这个词没问题,但容易让初学者误会:好像越自主越高级。
工程里不是这样。
真正可靠的 Agent,不是无限自治,而是受控协作。
它应该有:
- 明确的工具范围。
- 明确的参数 Schema。
- 明确的最大步数。
- 明确的错误处理。
- 明确的风险拦截。
- 明确的日志和 traceId。
模型可以决定下一步,但只能在程序允许的范围内决定。
对前端开发者来说,可以这样类比:
用户输入 = 用户事件
messages = 状态容器
modelOutput = 状态决策
tool_call = effect 描述
runTool = effect 执行器
final_answer = 渲染结果
maxSteps = 防止无限循环的保护
Agent 不是凭空多出来的外星架构。
它只是把语言模型放进了你熟悉的状态与副作用系统里。
10. 生产环境避坑指南
1. 不要让 Agent 无限循环
必须设置 maxSteps。
超过步数就停止,并返回结构化错误。
2. 不要把 tool error 当正常工具结果
工具失败时,要让模型知道这是失败,而不是把错误文本当成正常数据继续编。
建议在 tool message 里保留 ok、errorType、message 等字段。
3. 高风险工具不要自动执行
取消订单、发送邮件、修改权限、扣款、删除数据,都不应该在 Agent Loop 中无确认执行。
这类工具应该返回 confirmation_required,交给用户或业务规则确认。
4. 每一步都要记录 traceId
一次 Agent 请求可能包含多次模型调用和多次工具调用。
没有统一 traceId,排查时很难把它们串起来。
5. 步数越多,不代表能力越强
步数越多,成本、延迟和错误概率都会上升。
大多数业务型 Agent,先把 2 到 4 步跑稳定,比追求长链路自主规划更重要。
11. 常见误区
误区 1:一次大模型调用就是 Agent
不是。一次模型调用只是聊天或生成。Agent 至少要有决策、工具、状态和循环。
误区 2:工具执行完就结束
不一定。工具结果通常还要回到模型,让模型生成用户能读懂的最终回答。
误区 3:Agent 应该尽可能多跑几步
不是。步数越多,成本、延迟和错误概率都会上升。够用就好。
误区 4:先接真实模型才能学 Agent
不需要。先用 mock 模型跑通控制流,反而更适合学习和调试。
12. 给前端开发者的落地清单
如果你要在团队里实现一个最小 Agent,可以从这份清单开始:
- 明确 Agent 支持哪些任务。
- 定义可用工具和工具 Schema。
- 实现工具调度器。
- 设计模型输出类型:
tool_call或final_answer。 - 维护
messages。 - 工具结果写回上下文。
- 设置
maxSteps。 - 记录每一步 trace。
- 处理未知工具、参数错误和工具失败。
- 高风险工具必须走确认。
这份清单能帮你避免把 Agent 做成一个“看起来会自己想办法,实际上出了事没人知道在哪里”的黑盒。
结语
Agent 不是一次调用。
Agent 是一轮受控循环。
模型负责判断下一步,工具负责获取真实信息或执行动作,程序负责维护状态、控制边界、处理错误和决定什么时候停止。
对前端开发者来说,这件事并不陌生。你早就熟悉事件、状态、副作用和渲染。现在只是多了一个语言模型,帮助系统理解自然语言里的意图。
当你能写出一个最小 Agent Loop,能看懂每一步发生了什么,能让它在错误时停下来而不是乱跑,你就已经跨过了 Agent 工程最重要的一道门槛。