这篇文章想先讲清楚一件更基础的事:Agent 到底是什么;同时会按当前 Demo 给出一套能跟着跑通的搭建步骤,方便你自己验证 Agent Loop。
这个阶段在学什么
很长一段时间里,行业叙事容易把人往两个极端推:一边是「学会微调 / 训模型才算 AI」,一边是「会写 prompt 就算会做 Agent」。这两种说法对我这种写前端的人都不友好------前者门槛虚高,后者又把机制说薄了。真正动手写完一个最小 Demo 之后,判断会清晰很多:Agent 开发的核心不是训练模型,而是用代码编排 LLM。状态怎么传、工具何时调用、循环何时停止、失败时如何回传,这些都是软件工程问题。前端已经熟悉的 API 编排、接口契约、错误处理,在这里可以直接迁移,而且往往就是主战场。
下面我会用一个 TypeScript 的最小 Demo,把 Agent Loop 拆开看一遍。代码本身不长,重点是理解每一步在发生什么。同时也会说清楚:TypeScript 适合快速建立认知,但如果你认真要走 Agent 这条路,Python 仍然绕不过去。
先理清一个误解:Agent 不是聊天机器人
先对比两条链路。
普通对话助手(一问一答):
输入 → LLM → 输出 → 结束
就是我们平时用的那种:你问一句,它答一句,答完就结束。模型再强,也只是在文本空间里生成下一串 token。它不接触你的数据库,不执行你的业务函数,也不知道「刚才那次查询失败了」。
Agent:
markdown
输入 → LLM → 要不要用工具?
↓ 要
调用工具 → 把结果塞回上下文 → 再问 LLM → ...
↓ 不要
输出最终回答 → 结束
关键区别不在模型「更聪明」,而在于你的代码控制了一个循环:模型可以提出调用工具;工具由你的代码执行;结果再喂回模型;模型再决定继续调工具,还是给出最终答案。循环本身是工程结构,模型只是循环里的决策节点。
在 Vercel AI SDK 里,这个「继续 / 终止」会直接体现在每一步的 finishReason 上:
finishReason: "tool-calls":模型还想用工具,循环应继续finishReason: "stop":模型认为可以收尾,循环终止
所以 Agent 不是「会说话的模型」,而是「模型 + 工具 + 由代码驱动的决策循环」。普通对话助手的终点是一句话;Agent 的终点是一轮被约束过的协作过程。理解这一点,后面的 Demo 才有意义------否则你只会觉得「多写了几个 function」。
先把项目搭起来
技术栈:Next.js + Vercel AI SDK + Zod,模型走 OpenRouter。目录大致是这样:
bash
app/api/chat/route.ts # 调用 generateText,跑 Agent Loop
lib/tools/weather.ts # 天气工具
lib/tools/calculator.ts # 计算工具
.env.local # OPENROUTER_API_KEY
1. 初始化并安装依赖
bash
pnpm create next-app@latest ai-gateway-demo
cd ai-gateway-demo
pnpm add ai @openrouter/ai-sdk-provider zod
2. 配置 OpenRouter
去 OpenRouter 注册并创建 API Key,在项目根目录新建 .env.local:
bash
OPENROUTER_API_KEY=你的key
选一个支持 tool calling 的模型即可。我这边用的是免费档里的 qwen/qwen3-next-80b-a3b-instruct:free,你也可以换成 OpenRouter 上其他免费 / 付费模型。
3. 按下文写好三个文件后启动
bash
pnpm dev
浏览器打开 http://localhost:3000/api/chat。接口会返回最终 text;终端里会打印完整的 steps------重点看终端,那才是 Agent Loop 的过程。
下面三个小节对应这三个文件里最核心的部分。
最小 Demo:三个部分就能跑起来
(1)定义 Tool:lib/tools/weather.ts
以天气工具为例(计算器同理,放在 lib/tools/calculator.ts):
typescript
import { tool } from 'ai';
import { z } from 'zod';
const fakeWeather: Record<string, { temp: number; condition: string }> = {
'北京': { temp: 12, condition: '晴' },
'上海': { temp: 18, condition: '多云' },
'深圳': { temp: 26, condition: '阴' },
};
export const getWeather = tool({
description: '查询某个城市的当前天气。当用户询问天气相关问题时使用此工具。',
inputSchema: z.object({
city: z.string().describe('城市名称,例如 "北京"、"上海"'),
}),
execute: async ({ city }) => {
const data = fakeWeather[city];
if (!data) return { error: `暂无 ${city} 的天气数据` };
return { city, temperature: data.temp, condition: data.condition };
},
});
一个 Tool 本质上是三件事:
-
description这是模型做工具选择时的主要依据。写得含糊,模型就容易瞎调或不调;写得具体,选对工具的概率会明显提高。可以把它理解成「给模型看的接口文档」:标题说清能力,正文说清适用场景。前端写组件 props 文档时的那点认真劲,用在这里非常值。
-
inputSchema(Zod)定义参数结构。AI SDK 会把它转成 JSON Schema 发给模型;
.describe()里的文字会成为参数说明。模型根据这些说明决定传什么值------所以 schema 不只是类型检查,也是给模型读的契约。契约写清楚了,后面排错才有抓手。 -
execute真正执行逻辑的地方。模型只负责「决定调哪个工具、传什么参数」;查库、调 API、算式求值,都由你的代码完成。这里的天气数据是假的,换成真实天气 API 也完全成立------边界没变:模型决策,代码执行。
计算器工具(lib/tools/calculator.ts)结构完全一样:
typescript
import { tool } from 'ai';
import { z } from 'zod';
export const calculate = tool({
description: '计算数学表达式。当用户询问算术问题(加减乘除、幂运算等)时使用此工具。',
inputSchema: z.object({
expression: z.string().describe("数学表达式,例如 '12 * 34' 或 '(5 + 3) / 2'"),
}),
execute: async ({ expression }) => {
try {
const result = Function(`'use strict'; return (${expression})`)();
return { expression, result };
} catch {
return { error: `无法计算表达式: ${expression}` };
}
},
});
多工具并不改变机制,只是让循环里可能出现多次或并行的 tool call。机制懂了,工具数量只是业务扩展。
(2)调用 Agent Loop:app/api/chat/route.ts
核心就是一次 generateText:
typescript
import { generateText, stepCountIs } from 'ai';
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import { getWeather } from '@/lib/tools/weather';
import { calculate } from '@/lib/tools/calculator';
const openrouter = createOpenRouter({
apiKey: process.env.OPENROUTER_API_KEY,
});
export async function GET() {
const { text, steps } = await generateText({
model: openrouter('qwen/qwen3-next-80b-a3b-instruct:free'),
system: `你是一个助手,可以使用多个工具。
- 用户问天气时,调用 getWeather,不要编造天气信息。
- 用户问数学计算时,调用 calculate,不要自己心算。
- 如果一个问题同时涉及多个工具,依次调用它们,不要跳过。`,
prompt: '北京今天天气怎么样?另外帮我算一下 25 乘以 48。',
tools: { getWeather, calculate },
stopWhen: stepCountIs(5),
});
console.log(JSON.stringify(steps, null, 2));
return Response.json({ text });
}
几处值得单独看:
tools:把工具注册进去。模型只能看见你暴露的集合------权限边界由此划定。system:约束「何时必须用工具、禁止编造」。这和前端写业务规则很像,只是规则的消费者是模型,不是 if/else。stopWhen: stepCountIs(5):给循环加安全上限,防止工具来回调用停不下来。生产里这是成本与稳定性的硬约束,不是可选项。
generateText 在内部帮你跑完了那个循环。你拿到的 text 是最终回答;steps 则是每一轮「模型决策 →(可选)工具执行」的完整轨迹。第一次跑通时,建议先别急着看页面上的 text,先把终端里的 steps 读完------Agent 的「过程」比「答案」更值得看。
(3)读 steps:循环是怎么走完的
对上面那句 prompt,实际跑出来大致是两步。
Step 0:finishReason: "tool-calls"
模型判断需要工具,一次发出两个 tool call(天气和计算彼此独立,可以并行):
json
{
"finishReason": "tool-calls",
"toolCalls": [
{ "toolName": "getWeather", "input": { "city": "北京" } },
{ "toolName": "calculate", "input": { "expression": "25 * 48" } }
],
"toolResults": [
{ "toolName": "getWeather", "output": { "city": "北京", "temperature": 12, "condition": "晴" } },
{ "toolName": "calculate", "output": { "expression": "25 * 48", "result": 1200 } }
]
}
注意顺序:先有调用意图,再有 SDK 执行后的结果。模型并没有自己「查出」天气或「算出」乘法,它只是发出了结构化调用。真正查表、求值的,是你的 execute。
Step 1:finishReason: "stop"
工具结果被塞回上下文后,模型再生成最终文本,例如:「北京今天天气晴,温度 12℃。25 乘以 48 等于 1200。」此时 finishReason 变为 "stop",循环结束。
整条链路可以压成一句话:
模型提出调用 → 你的代码执行 → 结果回到模型 → 模型决定停或继续。
这就是 Agent Loop。Demo 代码很短,但「每一步在发生什么」都能看清楚------这比多写几个工具、换几个模型都更重要。搞不清 Loop,后面做复杂 Agent 时,你只会觉得「有时灵有时不灵」,却说不出该改 schema、改 system,还是改停止条件。
为什么说 Agent 开发是软件工程
把上面的 Demo 对照日常前端工作,对应关系其实很直接:
| 前端常见能力 | 在 Agent 里对应什么 |
|---|---|
| 接口契约 / 类型定义 | Tool 的 inputSchema + description |
| API 编排 / 流程控制 | Agent Loop、stopWhen、多工具调度 |
| 错误处理 | execute 返回 { error },让模型基于失败信息改口或重试 |
| 状态与上下文 | 每一步的 tool results 如何累积进下一轮 |
| 产品文案与边界 | system prompt:禁止编造、何时必须调工具 |
模型负责「在不确定环境里做选择」;工程负责「选择空间、执行环境、停止条件、失败路径」。
你不会去微调一个大模型来「学会查天气」------你会写一个 getWeather,让模型学会在合适的时候调用它。前者是模型训练问题,后者是系统集成问题。入门阶段把两者分清,能少走很多弯路。
这也是为什么我说:这一阶段的目标不是「学会写 Agent」,而是建立正确认知。代码可以很短;短到只剩几十行时,反而更容易看清机制。你要练的不是堆功能,而是能解释:
- 为什么这一步是
tool-calls而不是直接stop? - 参数是从 schema 的哪段描述推导出来的?
- 如果工具返回 error,下一轮模型会看到什么?
再往生产走,复杂度会迅速堆上来:工具权限、超时与重试、可观测性(把 steps 落日志)、提示词与 schema 的回归测试、并发与成本控制。这些全都不是「调模型玄学」,而是工程问题。前端经验在这里不是旁门,而是主路------只不过消费方从浏览器用户,变成了会出错、会幻觉、会重复调用的模型。
但 Python 仍然绕不过去
用 TypeScript 写这个 Demo,对我来说有一个很实际的原因:认知建立更快。工具链熟悉,类型系统顺手,一两个小时内就能看到完整的 Agent Loop,而不是先在环境与生态里转半天。对前端转 Agent 的人,这是很好的第一站。
但这不等于「以后 Agent 就这样写行了」。就我目前看到的情况------尤其是国内不少公司的 Agent / LLM 应用落地------生产侧主流仍是 Python:LangChain / LangGraph 一类编排框架、评测与数据链路、研究与业务示例、内部平台 SDK,文档和同事能直接复用的资产,大多围绕 Python。面试和协作里,别人扔给你的也经常是 Python notebook 或服务。
这一阶段该带走什么
- Agent ≠ 更强的聊天机器人 。差别在于代码驱动的工具调用循环,而不是模型本身。看到
tool-calls和stop,你就应该能在脑子里画出循环图。 description/ schema /execute/stopWhen才是你真正在写的东西。模型决策,代码执行------边界清楚,后面才好排错,也好和「训模型」这件事彻底分开。- Demo 用来建立认知;认真做 Agent,还是要面对 Python 生态。 两者不对立:先把 Loop 看懂,再换栈成本会低很多。
这个 Demo 本身并不复杂,难的不是把代码敲出来,而是看完 steps 之后,能用自己的话说明白:为什么会出现 tool-calls,结果怎么回去,循环何时变成 stop,以及你作为工程师到底在控制系统的哪一层。
搞清这些,才算真正入门------不是入门某个框架的 API,而是入门 Agent 作为一种软件形态。框架会换,Loop 的结构不会。