Agent 是什么:从一次 API 调用到自主系统

Agent 是什么:从一次 API 调用到自主系统

欢迎来到本教程。这一章不写代码原理,只回答三个问题:Agent 到底是什么为什么我们坚持不用框架、以及接下来 23 章你会走到哪里。同时我们把开发环境准备好,让本书的「可运行为王」从第一页就立起来——本章末尾就有一个能立刻跑起来的演示。

本章目标

读完本章并做完配套练习后,你应该能够:

开场反例:一个只会一问一答的 chatbot

假设你有一个用 OpenAI API 写成的「智能助手」,前端一个输入框,后端一段代码:

// a "chatbot": one request, one answer, then done.
const res = await fetch('https://api.openai.com/v1/chat/completions', {
  method: 'POST',
  headers: { authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
  body: JSON.stringify({
    model: 'gpt-4o',
    messages: [{ role: 'user', content: '你好,你是谁?' }],
  }),
});
const data = await res.json();
console.log(data.choices[0].message.content);

你问一句,它答一句,看起来像模像样。直到你提出一个稍微「绕弯」的需求:

「查一下明天北京的天气,再告诉我该不该带伞。」

这台 chatbot 会怎样?它只会把这句话原样发给模型,模型要么瞎编一个天气数字,要么诚实地说「我没有联网能力」。原因很简单:这个程序只有一次调用,没有第二步。它不会自己去「调用天气工具」,因为代码里压根没有工具;它也不会「根据查到的结果再推理」,因为根本没有第二轮对话。

要让上面的需求成立,程序至少要会三件事:

  1. 感知:把用户的意图「我需要查天气」识别出来;
  2. 决策:决定「我应该先调用一个天气查询工具」;
  3. 行动 + 观察:真的把工具调起来,把结果(比如「明天多云,13–20℃」)拿回来,再基于这个新信息生成最终答复。

一个只做「一问一答」的程序做不到这些,问题不在模型,而在程序的结构里没有这些环节。本教程要教你的,正是如何把这些环节一个一个手写出来,最后拼成一台真正的「自主系统」。

Agent 的最小定义

我们先给一个全书通用的最小定义,它不依赖任何特定框架或厂商:

Agent = 模型 + 环境(工具)+ 控制循环

flowchart LR
  sense[感知] --> decide[决策] --> act[行动] --> observe[观察] --> more{还有任务吗?}
  more -- 是 --> decide
  more -- 否 --> done[输出最终结果,结束]

把这个循环翻译成刚才的天气例子:

  1. 感知:读到用户消息「查一下明天北京的天气,再告诉我该不该带伞」;
  2. 决策:模型说「我要调用 get_weather 工具,参数 city=北京」;
  3. 行动:程序执行工具,拿到结果「明天多云,13–20℃」;
  4. 观察:把工具结果作为新消息回填给模型,回到第 2 步;
  5. 模型看到天气数据后决策:「可以输出最终答复了,不再调用工具」;
  6. 结束:输出「明天北京多云、13–20℃,无降雨,不用带伞。」

循环的每一次「下一步做什么」,都由模型根据当前观察自主决定,不由程序员预先写死。这就是 Agent 与普通程序的分水岭。

workflow vs agent:预设路径 vs 自主决策

关于「什么才算 Agent」,最容易混淆的是它和 workflow(工作流)的区别。Anthropic 的工程博客《Building effective agents》给了一个非常干净的划分,我们直接借用:

Workflows are systems where LLMs and tools are orchestrated through predefined code paths. Agents, on the other hand, are systems where LLMs dynamically direct their own processes and tool usage.

翻译过来:

用「餐厅」打比方:workflow 是流水线式快餐,菜从 A 窗口传到 B 窗口再传到 C 窗口,顺序是后厨定死的;agent 是大厨拿到订单后自己决定先备菜还是先烧锅、中途发现缺食材自己差人去拿。两者都有价值,但自主程度完全不同。

为什么这个区别很重要?因为很多号称「Agent」的产品其实只是 workflow。这本身没有错,但如果你分不清,就会在架构设计上犯错误:把该用 workflow 的简单场景硬做成 agent(过度设计),或者把需要 agent 自主决策的场景硬套成固定流水线(能力不足)。

为什么本教程 from-scratch 不用框架

现在市面上有大量现成的 Agent 框架(LangChain、LlamaIndex、Vercel AI SDK、各种 agent-* 库)。既然拼积木这么方便,为什么我们还要从零手写?

先看一个真实的调试场景。假设你用某个框架搭了一个 Agent,它跑起来后开始无限调用同一个工具

> tool_call: search_web("agent")
> tool_result: [50 results] ...    (内容太长,被截断)
> tool_call: search_web("agent")
> tool_result: [50 results] ...
> tool_call: search_web("agent")   ← 又来了,永远停不下来

你的第一个问题是:这个循环为什么没触发停止条件? 框架可能没有把 tool_result 完整回填给模型(上下文被截断了);可能是工具结果格式与框架期望的 schema 不匹配,模型永远「看不到」结果;可能是 max_iterations 默认值根本没设;也可能是 stop 判断逻辑本身就埋在框架深层的某个 while 里,你翻了半天源码也没找到它在哪——因为你从来没看过它是怎么写的

这种「无从下手」正是框架教学的最大成本:当系统行为与预期不符时,你调试的对象是一个你完全不了解的黑盒。你只能靠猜、靠搜索框架 issue、靠反复试参数,而不是靠「我知道它内部每一步应该发生什么」。

本教程选择相反的路线,理由有三:

  1. 原理优先于封装Agent Loop、工具系统、上下文管理、SSE 流式协议……每一层抽象都先手写一遍,让你看见「框架魔法」背后的真实机制。手写过一遍之后,你用任何框架都能立刻看穿它替你做的是什么、替你省了什么、又藏了什么坑。
  2. 可调试、可裁剪。你自己写的 50 行 Agent Loop,任何一行都可以打日志、改逻辑。真出问题时,你调试的是自己理解透彻的代码,而不是第三方源码。
  3. 框架最终会被你「征服」而不是「膜拜」。第 23 章我们会带你反过来去读主流框架的源码——到那时你会发现,框架里那些「智能」的编排逻辑,正是本书每一章手写过的零件。学习闭环到这儿就完成了。

说明:这不是「框架无用论」。真实生产中,成熟的框架帮你省掉大量工程琐事,非常值得用。本教程的立场是先手写、后选型——只有当你亲手造过一遍轮子,你才有资格判断哪个框架的抽象设计得更好、什么时候值得用它。用「先学加减乘除,再用计算器」来类比,最恰当不过。

全栈地图:这本书覆盖到什么程度

大多数 Agent 教程只讲「后端核心」(Agent Loop、工具调用、上下文)。但这只是半个 Agent。一台真正能用的 Agent,还需要:

本教程把这整条链路都纳入覆盖范围。以下是 23 章路线图(来自 PLAN.md,按两段式结构组织):

#章节一句话定位
01Agent 是什么:从一次 API 调用到自主系统本章:定义、边界、地图、环境准备
02最底层:用 HTTP 裸调 LLM只用 fetch 调 chat/completions,解剖请求响应
03流式输出:手写 SSE 客户端逐 chunk 解析,[DONE] 与中断
04厂商差异与 Provider 抽象统一 OpenAI / Anthropic 的消息与工具格式
05结构化输出与 JSON 校验提取、校验、兜底:把模型文本变成可信数据
06Tool Calling 手动全流程tool schema、调用解析、结果回填、并行调用
07最小 Agent Loop50 行 while 循环:think → act → observe
08工具系统工程化注册表、schema 生成、错误协议、内置工具集
09上下文工程token 估算、窗口 vs 压缩、system prompt 分层
10持久层:会话与事件存储事件日志、追加查询、崩溃恢复、会话回放
11记忆与检索手写向量检索,工作记忆 vs 长期记忆
12经典范式手写ReAct、Plan-and-Execute、Reflection 最小实现
13评估入门:手写 eval任务集、grader、失败模式分类、回归
14产品骨架 v01CLI 里组装完整 Loop,core 与界面分层
15服务化 v02事件协议设计,SSE 下发,多会话
16断线恢复与后台运行 v03事件序号与重连续传,后台运行
17Web 前端 v04原生 TS 流式渲染,中间态展示
18权限与 Human-in-the-Loop v05工具风险分级、审批交互、审计
19安全与沙箱 v06路径逃逸防护、Shell 隔离、prompt injection
20可观测性与成本 v07结构化日志、trace、token/成本核算
21MCP 接入 v08手写 JSON-RPC 2.0 over stdio
22多智能体 v09orchestrator-worker、handoff、并发代价
23收尾 v10:配置、部署与全书回顾部署、密钥管理、v01→v10 回顾、框架对照

第 01 章是绪论;第 02–13 章是阶段一「拆解零件」:每章一个自包含主题 demo,章节间弱耦合,你可以按兴趣挑着读。第 14–23 章是阶段二「装配整机」:一个主线产品从 v01 到 v10 逐章演进,每一章加一个能力,随时可以对比任意两个版本的 diff。这个「拆开零件 → 装成整机」的结构,保证你既有局部的手感,又有全局的视图。

最终产品预览:mini 全栈 Agent

这个教程的终点,是一台可以真正用的 mini 全栈 Agent

flowchart TD
  user[终端用户]
  user --> cli["CLI(第一交互面)"]
  user --> web["Web 前端(原生 TS)"]
  cli -- "事件协议(SSE)" --> core
  web -- "事件协议(SSE)" --> core
  core["Node 后端 · Agent Core<br/>Agent Loop · 工具系统 · HTTP/SSE 服务 · 审批"]
  core --> db["SQLite 持久层<br/>session / event 落库"]

你的学习闭环

本教程不只是「读完了事」,它配了一条可验证的学习闭环:

flowchart LR
  read[读章节] --> ex["做编码练习(自动判题)"] --> quiz["章节 quiz(≥80% 通过)"] --> exam["阶段综合考 ×2"] --> final["结业项目(从零实现 mini 全栈 Agent)"]

每一环都有机器可验证的反馈,绝不靠「感觉自己懂了」来判断进度。这个闭环就是整个网站(教程阅读、判题入口、进度追踪)要承载的东西。

环境准备

开始之前,确认你的机器满足以下条件:

  1. Node.js ≥ 20:教程的示例大量使用原生 fetchnode:http,Node 20+ 内置完整支持,示例尽量零依赖。 检查版本:

    node --version   # 应显示 v20.x 或更高

    若版本过低,请到 nodejs.org 安装 LTS 版本。

  2. 真实 API key:可选。第 02 章开始会涉及真实 LLM 调用。如果你有 OpenAI / Anthropic / 兼容服务商的 key,可以准备好(用环境变量管理,不要把 key 写进代码或提交到 git);如果没有,完全可以跳过,本教程的示例和判题都内置了 mock model 兜底。

  3. mock model 兜底机制:这是本教程的一个重要设计。所谓 mock model,就是一个确定性脚本模型:它不调用真实 LLM,而是按照固定规则回复(比如「复述用户最后一条消息并加上固定前缀」),通过一个模拟 OpenAI chat/completions 接口的本地服务暴露出来。它的价值在于:

    • 判题无需 API key:练习测试只跟 mock server 交互,任何人在任何环境都能跑出可复现的结果;
    • 结果确定:真实 LLM 的输出有随机性,没法用于自动化判题;脚本模型永远返回同样输入下的同样输出;
    • 机制等价:对你要实现的代码来说,mock 与真实模型走的是同一个 HTTP 协议、同一种消息结构,学会的知识 100% 迁移。

    第 02 章我们会详细解剖 chat/completions 协议;第 01 章你只需要体验一把「本地模型服务 + 客户端」的完整链路。

常见坑

坑一:把 chatbot 当 agent。 一个「一问一答」的 LLM 调用是 Agent 的零件,不是 Agent 本身。判断标准很简单:它会不会在运行时自己决定下一步行动? 不会,就还不是 agent。许多产品宣传里的「AI 助手」其实只是 chatbot,分辨不出来会让你在架构讨论中处处失焦。

坑二:还没懂原理就装框架。 初学者最常见的路线是「先 LangChain 一把梭」,结果遇到死循环、上下文爆炸、工具调用失灵时毫无排障能力,只能搜 issue 碰运气。先手写一遍 Agent Loop,再用框架,前后体验差距极大。如果你已经装了框架也没关系,本教程的每一章都会帮你把这些抽象拆开看一遍,回头再去看框架代码,很多「魔法」会变成「老朋友」。

小结

下一章(ch02)我们正式动手:只用 fetch 裸调一次 LLM,解剖请求与响应的每一个字段。在那之前,先去把本章的练习做掉、quiz 刷到 80 分——然后我们开始造轮子。

延伸阅读

完成阅读,去做练习 →