Agent 开发不是模型训练:一个前端的入门认知

这篇文章想先讲清楚一件更基础的事: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 本质上是三件事:

  1. description

    这是模型做工具选择时的主要依据。写得含糊,模型就容易瞎调或不调;写得具体,选对工具的概率会明显提高。可以把它理解成「给模型看的接口文档」:标题说清能力,正文说清适用场景。前端写组件 props 文档时的那点认真劲,用在这里非常值。

  2. inputSchema(Zod)

    定义参数结构。AI SDK 会把它转成 JSON Schema 发给模型;.describe() 里的文字会成为参数说明。模型根据这些说明决定传什么值------所以 schema 不只是类型检查,也是给模型读的契约。契约写清楚了,后面排错才有抓手。

  3. 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 或服务。


这一阶段该带走什么

  1. Agent ≠ 更强的聊天机器人 。差别在于代码驱动的工具调用循环,而不是模型本身。看到 tool-calls 和 stop,你就应该能在脑子里画出循环图。
  2. description / schema / execute / stopWhen 才是你真正在写的东西。模型决策,代码执行------边界清楚,后面才好排错,也好和「训模型」这件事彻底分开。
  3. Demo 用来建立认知;认真做 Agent,还是要面对 Python 生态。 两者不对立:先把 Loop 看懂,再换栈成本会低很多。

这个 Demo 本身并不复杂,难的不是把代码敲出来,而是看完 steps 之后,能用自己的话说明白:为什么会出现 tool-calls,结果怎么回去,循环何时变成 stop,以及你作为工程师到底在控制系统的哪一层。

搞清这些,才算真正入门------不是入门某个框架的 API,而是入门 Agent 作为一种软件形态。框架会换,Loop 的结构不会。

相关推荐
流浪0012 小时前
大模型技术全景(十八):RAG 检索增强生成与知识时效性问题
llm·大语言模型·rag
吠品2 小时前
HTTPS 真身:HTTP 套了层 TLS
java·服务器·前端
用户3126874877202 小时前
GPT-6 智能界面与 Haiku 5.5 降价 90%:10 月 7 日"模型之战"技术拆解
agent
JYeontu2 小时前
实现一个「曲线滑块验证」功能
前端·javascript·html
Csvn2 小时前
组件库(从 API 契约到 Headless 内核)
前端
deli0072 小时前
为什么最近邻路线总在绕远?2-opt 把 50 城 TSP 缩短了 20.9%
前端
易动讯2 小时前
手搓 Agent 系列 02|Tool 规模化的 3 个工程问题:成本、选择、维护
agent
guslegend2 小时前
底层数据设计:读写分离、分库分表与热点数据隔离
前端
Southern Wind2 小时前
AIAgent——第一章:本地 Agent 与 Vue 3 流式全栈实战
前端·javascript·vue.js