AI Agent零基础学习

  目录
  1. 1. 第 1 章 大语言模型与 AI 应用基础
    1. 1.1. 1.1 三个实验:先感受,再解释
      1. 1.1.1. 实验一:同一个问题,越问越准确——上下文
      2. 1.1.2. 实验二:模型不知道答案时——幻觉
      3. 1.1.3. 实验三:同一个问题,结果不稳定——随机性
    2. 1.2. 1.2 AI、机器学习、深度学习与大语言模型的区别
    3. 1.3. 1.3 生成原理:自回归与”下一个词预测”
      1. 1.3.1. 自回归(Autoregressive)生成
      2. 1.3.2. 训练时在学什么:交叉熵损失
      3. 1.3.3. 为什么这决定了 Agent 的一切
    4. 1.4. 1.4 Token:大模型怎么”切词”
      1. 1.4.1. BPE(Byte Pair Encoding)机制
      2. 1.4.2. Token 与成本、上限的直接关系
      3. 1.4.3. 工程启示
    5. 1.5. 1.5 深入调用原理:Prompt、Message 与角色(TS 可运行)
      1. 1.5.1. TS 可运行示例(统一封装,全书复用)
    6. 1.6. 1.6 上下文管理:Context Window 与 Context Budget
      1. 1.6.1. 机制:为什么”窗口大”≠”更好”(Lost in the Middle)
      2. 1.6.2. 工程三原则
    7. 1.7. 1.7 幻觉与采样:Temperature / Top-P / Top-K(logits 机制)
      1. 1.7.1. 采样管线(真正理解参数)
      2. 1.7.2. 工程决策表
      3. 1.7.3. 幻觉的两类来源
    8. 1.8. 1.8 模型选型:通用 / 推理 / 多模态 / 本地
      1. 1.8.1. 推理模型的机制差异(为什么贵但强)
      2. 1.8.2. 本地模型怎么选(无 Key / 隐私)
    9. 1.9. 1.9 深入 Transformer:自注意力与”理解上下文”
      1. 1.9.1. 核心思想:自注意力(Self-Attention)
      2. 1.9.2. QKV 机制(面试必考)
      3. 1.9.3. 多头注意力(Multi-Head)
      4. 1.9.4. 位置编码(Positional Encoding)
      5. 1.9.5. 为什么上下文窗口有上限
    10. 1.10. 1.10 上下文工程 vs 提示词工程(Agent 的哲学基础)
    11. 1.11. 1.11 简历怎么写 + 面试题 + 练习题
      1. 1.11.1. 简历写法
      2. 1.11.2. 面试题速查(本章范围内标准答法)
      3. 1.11.3. 练习题
      4. 1.11.4. 本章小结
  2. 2. 第 2 章 RAG:原理、向量数据库、混合检索与 Rerank
    1. 2.1. 2.1 为什么需要 RAG
      1. 2.1.1. 大模型的”知识墙”
      2. 2.1.2. RAG 的解法
      3. 2.1.3. 三个关键子问题(全书主线)
    2. 2.2. 2.2 Embedding 原理与实战(TS 可运行)
      1. 2.2.1. 什么是 Embedding
      2. 2.2.2. 为什么不是”关键词匹配”
      3. 2.2.3. TS 可运行示例
    3. 2.3. 2.3 向量检索原理:距离度量、相似度与 TopK
      1. 2.3.1. 常见相似度度量
      2. 2.3.2. TopK 检索过程
      3. 2.3.3. 为什么需要索引(向量库的价值)
      4. 2.3.4. 关键认知:TopK 太小漏召回,太大稀释上下文
    4. 2.4. 2.4 文档处理:解析、清洗、分块与版本更新
      1. 2.4.1. 管线四步
      2. 2.4.2. 分块策略(机制与取舍)
      3. 2.4.3. TS 可运行示例(递归分隔 + 元数据)
    5. 2.5. 2.5 向量数据库选型:Qdrant / Chroma / Milvus
    6. 2.6. 2.6 混合检索原理:BM25、向量召回与融合排序
      1. 2.6.1. 为什么单一检索不够
      2. 2.6.2. BM25 机制
      3. 2.6.3. 融合排序(RRF,Reciprocal Rank Fusion)
    7. 2.7. 2.7 实战:用 Qdrant 实现 BM25 + 混合检索(TS 全可运行)
      1. 2.7.1. 起服务
      2. 2.7.2. 索引 + 混合检索
    8. 2.8. 2.8 查询优化:Query Rewrite 与 Multi-Query
      1. 2.8.1. 三种主流优化
      2. 2.8.2. Multi-Query 示例(TS)
    9. 2.9. 2.9 Rerank 重排:精排与上下文压缩
      1. 2.9.1. 为什么需要 Rerank
      2. 2.9.2. 机制:交叉编码器(Cross-Encoder)
      3. 2.9.3. TS 可运行示例(用 Jina/Cohere 或本地 bge-reranker)
    10. 2.10. 2.10 答案生成:来源引用与拒答机制
      1. 2.10.1. 生成时要做三件事
      2. 2.10.2. 工程实现要点(TS 思路)
    11. 2.11. 2.11 RAG 质量评估:Recall@K、MRR、Faithfulness(RAGAS)
      1. 2.11.1. 检索评估(离线,需标注)
      2. 2.11.2. 生成评估(可用 LLM 打分:RAGAS)
    12. 2.12. 2.12 实战:企业知识库(文档更新、权限隔离、混合检索)
      1. 2.12.1. 需求
      2. 2.12.2. 架构
      3. 2.12.3. 关键实现点(TS 片段)
    13. 2.13. 2.13 简历怎么写 + 面试题
      1. 2.13.1. 简历写法
      2. 2.13.2. 面试题速查
      3. 2.13.3. 练习题
  3. 3. 第 3 章 MCP、Tool Calling 与能力连接
    1. 3.1. 3.1 为什么 Agent 要”连工具”
    2. 3.2. 3.2 Tool Calling 原理:手写循环调用链(不依赖框架)
      1. 3.2.1. 机制:模型怎么”要求”调工具
      2. 3.2.2. TS 手写循环(可运行,不依赖 LangChain)
    3. 3.3. 3.3 MCP 架构:Host / Client / Server 与能力发现
      1. 3.3.1. 能力发现(Capability Discovery)
    4. 3.4. 3.4 MCP Server 能力建模:Tools / Resources / Prompts
      1. 3.4.1. Tools vs Resources(关键区分,面试常考)
    5. 3.5. 3.5 MCP Host 实战:工具发现、Schema 映射与调用循环(TS)
      1. 3.5.1. 3.5.1 一个最小 Server
      2. 3.5.2. 3.5.2 Host 侧:发现工具 + 映射 + 调用循环
    6. 3.6. 3.6 MCP 连接方式:stdio / Streamable HTTP / 远程
    7. 3.7. 3.7 实战:MCP 退款审核(人工确认、长任务)
      1. 3.7.1. 人工确认(Human-in-the-Loop)
      2. 3.7.2. 长任务(Long-running)
    8. 3.8. 3.8~3.10 实战:企业售后 MCP(结构拆解 / 权限隔离与幂等写入 / 长任务与 MCP Apps)
      1. 3.8.1. 3.8 项目演示与结构拆解
      2. 3.8.2. 3.9 权限隔离与幂等写入(硬核工程点)
        1. 3.8.2.1. 权限隔离:工具层做校验,不信任模型
        2. 3.8.2.2. 幂等写入:防止重复执行
      3. 3.8.3. 3.10 长任务、MCP Apps 与 Agent Host
    9. 3.9. 3.11 简历怎么写 + 面试题
      1. 3.9.1. 简历写法
      2. 3.9.2. 面试题速查
      3. 3.9.3. 练习题
  4. 4. 第 4 章 Agent 执行原理、任务规划与失败恢复
    1. 4.1. 4.1 从”模型”到”Agent”
    2. 4.2. 4.2 ReAct Agent:推理、行动、观察
      1. 4.2.1. 与思维链(Chain-of-Thought)的区别
      2. 4.2.2. TS 实现(在 3.2 循环基础上加”思考”字段)
    3. 4.3. 4.3 任务规划:从 ReAct 到 Plan-and-Execute
      1. 4.3.1. ReAct 的局限:走一步看一步
      2. 4.3.2. Plan-and-Execute:先规划,再执行
      3. 4.3.3. 何时用哪种(工程决策)
    4. 4.4. 4.4 Agent Runtime:运行状态、执行预算与终止条件
      1. 4.4.1. 1. 执行预算(Budget)
      2. 4.4.2. 2. 终止条件(Termination)
      3. 4.4.3. 3. 运行状态(State)
    5. 4.5. 4.5 结果校验:空结果、证据冲突与失败恢复
      1. 4.5.1. 常见失败与处理
      2. 4.5.2. 失败恢复机制
    6. 4.6. 4.6~4.8 实战:AI 编程 Agent(TS)
      1. 4.6.1. 4.6 需求与工具
      2. 4.6.2. 4.7 ReAct + 执行控制
      3. 4.6.3. 4.8 失败恢复、人工确认与执行轨迹
    7. 4.7. 4.9 简历怎么写 + 面试题
      1. 4.7.1. 简历写法
      2. 4.7.2. 面试题速查
      3. 4.7.3. 练习题
  5. 5. 第 5 章 LangChain / LangGraph:核心能力、运行原理与源码解析
    1. 5.1. 5.1 框架如何选型:LangChain / LangGraph / 自研
    2. 5.2. 5.2 核心组件:模型、消息、工具与 Runnable
      1. 5.2.1. Runnable 接口(LangChain 的统一抽象)
    3. 5.3. 5.3 createAgent 原理:执行循环与流式输出
      1. 5.3.1. createAgent 在内部做什么(机制)
    4. 5.4. 5.4 数据隔离:State 与 Runtime Context
      1. 5.4.1. 为什么需要 State
      2. 5.4.2. State 与”发给模型的上下文”分离
    5. 5.5. 5.5 Middleware 原理:Hook、拦截与结果校验
      1. 5.5.1. Middleware 能做什么(工程价值)
    6. 5.6. 5.6 LangGraph 状态图执行模型
      1. 5.6.1. 三要素
    7. 5.7. 5.7 LangChain × LangGraph 分层编排
    8. 5.8. 5.8~5.11 createAgent 源码解析(进阶硬核)
      1. 5.8.1. 5.8 初始化与状态 Schema
      2. 5.8.2. 5.9 Middleware 与图编译
      3. 5.8.3. 5.10 模型调用与 Middleware 执行
      4. 5.8.4. 5.11 ToolNode 与工具执行循环
    9. 5.9. 5.12 LangChain 生态:LangSmith 与集成
    10. 5.10. 5.13 上下文工程在 LangChain 的落地
    11. 5.11. 5.14 简历怎么写 + 面试题
      1. 5.11.1. 简历写法
      2. 5.11.2. 面试题速查
      3. 5.11.3. 练习题
  6. 6. 第 6 章 Agent Memory、会话恢复与 Agentic RAG
    1. 6.1. 6.1 从聊天记录到可用记忆
    2. 6.2. 6.2 短期记忆与会话恢复:Thread 与 Checkpointer
      1. 6.2.1. Thread(线程):会话 ID
      2. 6.2.2. Checkpointer:可恢复的运行状态
    3. 6.3. 6.3 上下文压缩:消息裁剪、自动摘要与 Compaction
      1. 6.3.1. 三种策略
    4. 6.4. 6.4 长期记忆:Store、用户画像与跨会话保存
      1. 6.4.1. Store:长期记忆的存储
      2. 6.4.2. 用户画像
    5. 6.5. 6.5 记忆提取:该记住什么、何时写入
      1. 6.5.1. 记住什么(不是所有都记)
      2. 6.5.2. 何时写入(时机)
    6. 6.6. 6.6 记忆更新与遗忘:去重、冲突修正与删除
    7. 6.7. 6.7 记忆召回:如何选择本次需要的信息
    8. 6.8. 6.8 Agentic RAG:按需检索、多轮补查与证据判断
      1. 6.8.1. 普通 RAG vs Agentic RAG
      2. 6.8.2. Agentic RAG 的流程
    9. 6.9. 6.9~6.11 实战:AI 面试教练(TS)
      1. 6.9.1. 6.9 训练流程与会话恢复
      2. 6.9.2. 6.10 记忆提取与用户画像
      3. 6.9.3. 6.11 跨会话复测与 Agentic RAG
    10. 6.10. 6.12 简历怎么写 + 面试题 + 练习题
      1. 6.10.1. 简历写法
      2. 6.10.2. 面试题速查
      3. 6.10.3. 练习题
  7. 7. 第 7 章 企业 Agent 工作流:DDD、持久化执行与 Human-in-the-Loop
    1. 7.1. 7.1 为什么企业 Agent 需要”暂停、修改、继续”
    2. 7.2. 7.2 业务建模与 DDD:领域模型与审核规则
      1. 7.2.1. 为什么需要 DDD(领域驱动设计)
      2. 7.2.2. 核心概念(面试常考)
      3. 7.2.3. 审核规则(业务规则)
    3. 7.3. 7.3 LangGraph 持久化执行:服务重启后如何恢复
      1. 7.3.1. 持久化 = Checkpointer 落盘
      2. 7.3.2. 恢复的意义
    4. 7.4. 7.4 人工审核:暂停、批准、修改与恢复(HITL)
      1. 7.4.1. HITL(Human-in-the-Loop)流程
      2. 7.4.2. LangGraph 的 interrupt(暂停)
    5. 7.5. 7.5 失败恢复:部分成功与局部重做
      1. 7.5.1. 问题:一个长流程中间某步失败
      2. 7.5.2. 实现
    6. 7.6. 7.6~7.8 实战:AI 演示文稿 Agent(TS)
      1. 7.6.1. 7.6 需求
      2. 7.6.2. 7.7 大纲审核与持久化执行
      3. 7.6.3. 7.8 失败续做、局部修改与导出
    7. 7.7. 7.9 简历怎么写 + 面试题 + 练习题
      1. 7.7.1. 简历写法
      2. 7.7.2. 面试题速查
      3. 7.7.3. 练习题
  8. 8. 第 8 章 复杂任务 Agent:Skills、Workspace 与多 Agent 协作
    1. 8.1. 8.1 复杂任务为什么需要多 Agent
    2. 8.2. 8.2 Agent Harness:为什么复杂任务需要它
    3. 8.3. 8.3 Agent Workspace:文件化上下文
      1. 8.3.1. 为什么需要 Workspace
      2. 8.3.2. 文件化上下文的价值
    4. 8.4. 8.4 Agent Skills:把方法封装成可复用能力
      1. 8.4.1. 什么是 Skill
      2. 8.4.2. 和 Tool / MCP 的区别
    5. 8.5. 8.5 多 Agent 协作模式:Router / Handoff / Supervisor / Subagent 怎么选
      1. 8.5.1. 怎么选(决策)
    6. 8.6. 8.6 多 Agent 结果治理:委派、产物契约与 Reviewer 返工闭环
      1. 8.6.1. 1. 任务委派(Delegation)契约
      2. 8.6.2. 2. 产物契约(Artifact Contract)
      3. 8.6.3. 3. Reviewer 返工闭环
    7. 8.7. 8.7~8.10 实战:多 Agent 剧情游戏(TS)
      1. 8.7.1. 8.7 需求与演示
      2. 8.7.2. 8.8 Workspace、Skills 与主 Agent
      3. 8.7.3. 8.9 Subagent 任务委派
      4. 8.7.4. 8.10 一致性审核、返工与构建
    8. 8.8. 8.11 简历怎么写 + 面试题 + 练习题
      1. 8.8.1. 简历写法
      2. 8.8.2. 面试题速查
      3. 8.8.3. 练习题
      4. 8.8.4. 全书小结

AI Agent应用开发系统教程

从大模型原理、RAG、MCP,到 Agent 执行原理、LangChain/LangGraph、Memory,再到企业级工作流与多 Agent 协作。

第 1 章 大语言模型与 AI 应用基础

本章是整套教程的地基。我们不满足于”背概念”,而是通过三个可复现的实验让你亲手感受大模型的行为,再深入到机制层解释它为什么这么做。
学完本章,你将能:从”下一个词预测”的机制层面解释大模型为何如此表现、理解 Token/上下文/幻觉/温度/注意力这些高频词的原理与工程含义,并独立选择模型与采样参数。

本章地图:

  • 1.1 三个实验:先感受,再解释(上下文、幻觉、随机性)
  • 1.2 概念澄清:AI / ML / DL / LLM
  • 1.3 生成原理:自回归、下一个词预测(含损失与采样)
  • 1.4 Token:分词、BPE 与成本(含词表与 id 映射)
  • 1.5 调用原理:Prompt、Message 与角色(TS 可运行示例)
  • 1.6 上下文管理:Context Window 与 Context Budget(含 Lost in the Middle 机制)
  • 1.7 幻觉与采样:Temperature / Top-P / Top-K(含 logits 原理)
  • 1.8 模型选型:通用 / 推理 / 多模态 / 本地(含推理模型思维链机制)
  • 1.9 深入 Transformer:自注意力、QKV、多头、位置编码
  • 1.10 上下文工程 vs 提示词工程(Agent 的哲学基础)
  • 1.11 简历 + 面试 + 练习

1.1 三个实验:先感受,再解释

打开 DeepSeek、豆包或 ChatGPT,随便问一句,几秒得到像样的回答;追问,它还记得上下文。但它是凭什么做到的?先做三个零代码实验,用任意支持多轮对话的产品即可(建议关闭联网搜索以观察纯模型行为)。

实验一:同一个问题,越问越准确——上下文

新建对话,输入:

1
介绍一下 NestJS。

得到通用回答(可能提到 Node.js、TypeScript、模块化、依赖注入)。注意:它不知道你为什么问,也不知道你的背景。

接着在同一对话里补充:

1
2
我是一名前端开发者,没有后端开发经验。
不要先讲依赖注入,请用前端开发者容易理解的方式重新解释。

回答立刻变化——它可能拿 React/Vue 组件类比控制器和服务。再输入:

1
还是有点长,压缩成三句话。

它会按要求改写。

关键动作:新建独立对话,只输入”还是有点长,压缩成三句话”。这次它懵了——不知道你要压缩什么。

结论(贯穿全书):模型每次生成都只参考它当前能看到的全部信息(Context)。它不是在回答孤立问题,而是在”根据本次收到的全部内容续写”。

实验二:模型不知道答案时——幻觉

新建对话,输入编造的问题:

1
星河零售公司的"蓝鲸退款规则"具体是什么?

不同模型反应不同:有的说不知道、有的要求补充资料、有的会现场编出一套看似正式的回答——这就是 AI 幻觉。

结论:模型是”续写器”不是”数据库”。它不查证,只生成”最像样的续写”。这是第 2 章 RAG 要解决的核心里程碑。

实验三:同一个问题,结果不稳定——随机性

1
给我写一句关于秋天的诗。

多问几次,结果每次都不同;把”温度/temperature”调高,差异更大。

结论:生成是概率采样而非确定性查表。采样参数决定随机程度,直接影响 Agent 的可复现性与测试。


1.2 AI、机器学习、深度学习与大语言模型的区别

高频概念澄清题,也是简历”基础能力”第一关。

1
2
3
4
5
graph TD
A[AI 人工智能<br>让机器完成智能任务的学科] --> B[ML 机器学习<br>从数据学规律,非显式编程]
B --> C[DL 深度学习<br>多层神经网络自动提特征]
C --> D[LLM 大语言模型<br>基于 Transformer、海量文本预训练的巨模型]
D --> E[生成式 AI / Agent 应用]
层级 定义 代表
AI 让机器表现出智能的学科 搜索、规划、视觉、NLP
ML AI 子集:从数据学规律 线性回归、决策树、SVM
DL ML 子集:多层神经网络自动学特征 CNN、RNN、Transformer
LLM DL 产物:Transformer + 海量文本预训练 GPT、Claude、DeepSeek、Qwen

一句话:LLM 是深度学习在”语言生成”上的巨量化应用;Agent 是站在 LLM 之上、能调用工具与规划的工程形态。


1.3 生成原理:自回归与”下一个词预测”

大模型的本质是一个巨大的条件概率分布:给定前面所有 Token,预测下一个 Token 的概率。

1
P(下一个token | 前面所有token)

自回归(Autoregressive)生成

模型逐个 Token 生成,每生成一个就拼回去,再预测下一个,直到遇到结束符(<EOS>)或达到长度上限。

以”北京是中国的”为例:

1
2
3
4
北京是中国的 → 首都   (P≈0.4)
→ 大都市 (P≈0.25)
→ 一个 (P≈0.1)
→ ... (长尾)

训练时在学什么:交叉熵损失

预训练目标可以理解为:对语料里每个位置,让模型最大化”真实下一个 Token”的概率。

1
L = -Σ log P(真实下一个token | 上文)

模型参数(千亿级)通过反向传播不断调整,使”对真实续写的预测概率”越来越高。这就是”它很擅长续写”的来源。

为什么这决定了 Agent 的一切

  1. 输出是”生成”不是”检索” → 需要外部证据(RAG)和校验来对抗幻觉。
  2. 长度越远越易漂移 → 工具结果要截断,长输出要分步。
  3. 上下文决定一切 → 你喂什么,它续写什么。这是整个教程的哲学根基。
  4. 采样是概率的 → 同输入不同输出,测试要容忍差异或固定 seed / 降温度。

TS 视角:你在 Node 里拿到的响应是一个流式增量序列(每块是一个增量 token),而不是一次性全文——这正是自回归的外在表现,第 5 章 createAgent 的”流式输出”就在消费这个流。


1.4 Token:大模型怎么”切词”

模型不按”字/单词”处理,而按 Token——词汇表里的最小单元,由分词器决定。

BPE(Byte Pair Encoding)机制

  1. 从训练语料统计字节/子词对的共现频率。
  2. 反复合并最高频的相邻对,直到达到目标词表大小(如 32K/128K)。
  3. 词被拆成子词:unbelievable → un + believ + able。
  4. 每个子词映射到整数 ID,模型处理的是一串整数。

为什么用子词而非单词:单词表会爆炸(数百万且无法覆盖新词);子词在”词表大小”与”覆盖度”间平衡,且能处理没见过的词(OOV)。

Token 与成本、上限的直接关系

概念 说明
上下文窗口 一次能处理的最大 Token 数(如 128K / 1M)
计费 多数 API 按输入 + 输出 Token 计费
中文 vs 英文 中文每个汉字常为 1~2 Token,英文 1 词约 1~2 Token;中文更费 Token

工程启示

Token 是 Agent 的”金钱与内存“。上下文塞太满 → 成本高、速度慢、关键信息被挤出(1.6)。做长文档时,先按 Token 估算预算,再决定检索/压缩策略。


1.5 深入调用原理:Prompt、Message 与角色(TS 可运行)

现代 API 输入是一个 Messages 数组,每条消息带 role。角色塑造对话结构,且优先级不同。

1
2
3
4
5
6
[
{ role: "system", content: "你是严谨的财务分析助手,只依据给定数据回答。" },
{ role: "user", content: "帮我分析这份报表。" },
{ role: "assistant", content: "好的,请提供报表数据。" },
{ role: "user", content: "上季度营收 1000 万……" },
]
角色 作用 可被用户覆盖?
system 设定身份/规则/约束,最高优先级 不应被覆盖,需防注入
user 用户输入 / 工具返回 —
assistant 模型历史回答(多轮记忆靠它) —
tool 工具执行结果回填 —

TS 可运行示例(统一封装,全书复用)

1
2
3
4
5
# 依赖安装(Node 18+,ESM)
npm init -y
npm i openai dotenv
npm i -D tsx @types/node
# 然后:node --env-file=.env 或 tsx 运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// llm.ts —— 统一 LLM 客户端封装(OpenAI / DeepSeek / Qwen / Ollama 通用)
import OpenAI from "openai";
import "dotenv/config";

export type Role = "system" | "user" | "assistant" | "tool";

export interface Msg {
role: Role;
content: string;
tool_call_id?: string;
}

export const client = new OpenAI({
baseURL: process.env.BASE_URL ?? "https://api.deepseek.com/v1",
apiKey: process.env.API_KEY ?? "EMPTY", // Ollama 本地时随便填
});

export async function chat(
messages: Msg[],
opts: { model?: string; temperature?: number; stream?: boolean } = {},
): Promise<string> {
const resp = await client.chat.completions.create({
model: opts.model ?? process.env.MODEL ?? "deepseek-chat",
messages: messages as any,
temperature: opts.temperature ?? 0.7,
stream: opts.stream ?? false,
});
return (resp as any).choices[0]?.message?.content ?? "";
}

// run.ts —— 用法
import { chat } from "./llm";

const messages = [
{ role: "system", content: "你是只讲重点的技术教练。" },
{ role: "user", content: "用一句话解释什么是 RAG。" },
] as const;

console.log(await chat([...messages]));

✅ 无 Key 可跑(Ollama):ollama run qwen2.5:7b,设 BASE_URL=http://localhost:11434/v1、MODEL=qwen2.5:7b,即可本地跑通全部示例。
⚠️ TS 提示:messages 传入前要转成 OpenAI 兼容结构,tool_call_id 在 tool 消息里必填(第 3 章详解)。


1.6 上下文管理:Context Window 与 Context Budget

  • Context Window(窗口):模型一次能处理的最大 Token 数,硬上限。
  • Context Budget(预算):你主动决定窗口里装多少、装什么。窗口是 1M 也不该全塞满。

机制:为什么”窗口大”≠”更好”(Lost in the Middle)

研究(Lost in the Middle,2023)发现,模型对上下文中间部分注意力最弱、首尾最强。窗口越大,中间信息越可能”被淹没”。

用自注意力解释:注意力权重会随距离与位置衰减,且当候选过多时,模型难以聚焦真正相关的片段。信息越多,信噪比越低。

工程三原则

  1. 只放必要信息:最相关内容放最前或最后(黄金位)。
  2. 压缩:长文档先摘要、先检索(RAG 的意义)。
  3. 预算分层:给 system / 历史 / 工具结果设上限,超出裁剪或摘要。

这是 Agent 的核心难点:在有限 Budget 里装下”回答当前问题最需要的信息”。第 2、6 章系统解决。


1.7 幻觉与采样:Temperature / Top-P / Top-K(logits 机制)

采样管线(真正理解参数)

模型输出的最后一步是给每个候选 Token 一个原始分数(logits),然后:

1
2
3
4
5
flowchart LR
A[logits 原始分数] --> B[Softmax 归一化为概率]
B --> C{Temperature 缩放}
C --> D{Top-P 核采样裁剪}
D --> E[从候选里采样一个 token]
  1. Temperature:将 logits 除以 T 再 softmax。
    • T<1:分布变”尖”,高概率更高,更确定。
    • T>1:分布变”平”,低概率也有机会,更多样。
    • T→0:贪心,永远选最高概率(接近确定性)。
  2. Top-P(核采样):只保留累计概率达 P 的最小候选集,然后重新归一化采样。
  3. Top-K:只保留概率最高 K 个(多数 API 不直接暴露)。

工程决策表

场景 temperature 原因
事实问答 / Agent 工具调用 0~0.3 需稳定可复现
代码生成 0~0.2 语法正确优先
头脑风暴 / 文案 0.7~1.0 要多样性
对话闲聊 0.5~0.8 平衡

幻觉的两类来源

  1. 事实性幻觉:编造不存在的事实/引用/数据 → 靠 RAG + 来源引用 + 校验(第 2 章)。
  2. 忠实性幻觉:不遵循给定资料,自己发挥 → 靠 system 约束 + 结果校验 + 低 temperature。

关键认知:temperature 不能根治幻觉,只降低随机发散。真正防幻觉 = 给足证据 + 强制引用 + 校验闭环。把”防幻觉”寄托在调参上是常见的工程误区。


1.8 模型选型:通用 / 推理 / 多模态 / 本地

选错模型是 Agent 项目最常见的第一坑。

维度 通用对话 推理模型 多模态 开源本地
代表 GPT-4o/Claude/DeepSeek-V3/Qwen DeepSeek-R1/o1/o3 GPT-4o/Gemini/Qwen-VL Qwen2.5/Llama/R1-蒸馏
强项 快、便宜、通用 复杂推理/数学/代码 图/音/视频输入 隐私、离线、低成本
弱点 复杂推理易错 慢、贵、Token 消耗大 更贵、深度推理弱 能力上限低、需算力
适合 多数 RAG/Agent 规划、数学、长链推理 截图/文档理解 数据敏感、边缘、调优

推理模型的机制差异(为什么贵但强)

推理模型(如 R1/o 系列)在生成最终答案前,先输出一段”思考过程(reasoning trace)”——它会自我规划、分步求解、自我校验,再把结论精简输出。代价是思考 Token 也计费、延迟高。

决策:

  • 要用:多步推理、需要自验证(复杂 SQL、数学、规划)。
  • 不要用:简单检索/分类/抽取——通用模型更快更便宜,推理模型反而慢且贵。

本地模型怎么选(无 Key / 隐私)

  • Ollama 一键跑:ollama pull qwen2.5:7b。
  • 个人机器 7B~14B;有独显可 32B+。
  • 评测口径:跑本地模型前先建你的任务样例集(20~50 条),过了再上线,别只看榜单分。

1.9 深入 Transformer:自注意力与”理解上下文”

面试若问”大模型为什么能处理上下文”,答出自注意力机制即可加分。

核心思想:自注意力(Self-Attention)

让每个 Token 去”看”其他所有 Token,算它们与自己的相关性权重,再按权重融合信息。

以”银行信贷部”和”我在河边散步”中的”银行”为例:

  • “信贷” → 模型把”银行”理解为金融机构。
  • “河边” → 模型把”银行”理解为河岸。

每个位置都能动态加权整段上文——这就是”理解上下文”的机制来源。

QKV 机制(面试必考)

每个 Token 生成三个向量:

  • Q(Query,查询):我在找什么?
  • K(Key,键):我能提供什么?
  • V(Value,值):我携带的内容。

注意力权重 = 当前 Token 的 Q 与所有 Token 的 K 做点积(再 softmax 归一化),用来加权 V:

1
Attention(Q,K,V) = softmax(Q·Kᵀ / √d_k) · V

√d_k 是缩放因子,防止点积过大导致 softmax 饱和。

多头注意力(Multi-Head)

多组 QKV 并行,各自关注不同方面(语法、指代、语义),再拼接融合。这让模型能同时捕捉多种关系。

位置编码(Positional Encoding)

注意力本身无序(是集合操作),必须注入位置信息才能知道”先后”。早期用正弦位置编码,现在多用 RoPE(旋转位置编码)。

为什么上下文窗口有上限

自注意力的计算量随序列长度平方增长(每 Token 要跟每 Token 算权重)。窗口越大,算力/内存压力越大——这是窗口上限的物理根因。

这个基础会让第 5 章 LangGraph 的状态传递、第 6 章记忆压缩更易理解:上下文是稀缺资源,要精打细算。


1.10 上下文工程 vs 提示词工程(Agent 的哲学基础)

付费课多半只讲”提示词工程”。但 Agent 时代,决定上限的是上下文工程。

提示词工程 上下文工程
对象 写 system/指令让模型听话 管理”模型能看到什么”的信息系统
手段 指令、示例、角色、few-shot 检索、过滤、压缩、排序、记忆、工具结果注入
解决 表达不清、格式不对 信息缺失、过载、冲突
典型 “你是客服,按 JSON 输出” “从 10 万条文档里挑相关 5 条,再拼进上下文”

结论:提示词工程教你怎么”说话”;上下文工程教你怎么”喂信息”。Agent 的能力 ≈ 上下文工程能力。这也是本教程从第 2 章起反复强调 RAG、记忆、压缩的原因。


1.11 简历怎么写 + 面试题 + 练习题

简历写法

✅ 好写法(体现工程理解):

“熟悉大模型调用与上下文工程:结合 RAG 与查询改写,将业务知识库检索准确率提升 XX%,通过来源引用与拒答机制把幻觉率降到可接受范围。”

❌ 差写法:

“熟悉 GPT、Transformer、注意力机制。”(只堆名词)

面试题速查(本章范围内标准答法)

问题 答题要点(章节)
大模型怎么生成? 自回归、逐个 Token 条件概率采样(1.3)
训练在学什么? 交叉熵损失,最大化真实续写概率(1.3)
什么是 Token?为什么中文更费? BPE 子词;中文子词切分更长(1.4)
system/user/assistant 区别? 角色与优先级,system 防注入(1.5)
窗口和预算区别? 硬上限 vs 主动管理;Lost in the Middle(1.6)
temperature 能解决幻觉吗? 不能,只降发散;防幻觉靠证据+校验(1.7)
什么时候用推理模型? 多步推理/自验证;避免简单任务滥用(1.8)
为什么 Transformer 能理解上下文? 自注意力动态加权,QKV+多头+位置编码(1.9)
提示词工程和上下文工程关系? 前者说话、后者喂信息;Agent 依赖后者(1.10)

练习题

  1. 动手(TS):用 1.5 的 chat(),分别 temperature=0 和 1.0 问”1+1 等于几”,观察差异。
  2. 动手:做 1.1 实验一,确认”新对话后模型不记得上文”。
  3. 分析:你的项目里哪些任务用推理模型、哪些用通用模型?给理由。
  4. 计算:估算 1000 字中文输入约多少 Token,再按你用的 API 单价估算一次调用成本。
  5. 进阶:用 stream: true 体验流式增量输出,观察自回归的逐 token 现象。

本章小结

  • 大模型 = 自回归”下一个词”概率生成器,不是数据库。
  • 上下文决定一切:能看到什么,才续写什么。
  • 幻觉、随机性、上下文窗口是三个必懂的天花板,各自有工程解法。
  • 提示词工程教说话,上下文工程教喂信息——Agent 拼的是后者。

第 2 章 RAG:原理、向量数据库、混合检索与 Rerank

RAG(Retrieval-Augmented Generation,检索增强生成)是把”外部知识”可靠装进上下文的最主流方案,也是 AI 应用开发工程师的第一道硬门槛。
本章从”为什么需要 RAG”讲起,深入机制层(embedding 到底是什么、距离度量怎么算、混合检索为什么比单一检索强),并给出全 TypeScript 可运行的完整链路,最终落成一个”文档更新 + 权限隔离 + 混合检索”的企业知识库。

本章地图:

  • 2.1 为什么需要 RAG
  • 2.2 Embedding 原理与实战(TS 可运行)
  • 2.3 向量检索原理:距离度量、相似度与 TopK
  • 2.4 文档处理:解析、清洗、分块与版本更新
  • 2.5 向量数据库选型:Qdrant / Chroma / Milvus
  • 2.6 混合检索原理:BM25、向量召回与融合排序
  • 2.7 实战:用 Qdrant 实现 BM25 + 混合检索
  • 2.8 查询优化:Query Rewrite 与 Multi-Query
  • 2.9 Rerank 重排:精排与上下文压缩
  • 2.10 答案生成:来源引用与拒答机制
  • 2.11 RAG 质量评估:Recall@K、MRR、Faithfulness(RAGAS)
  • 2.12 实战:企业知识库(文档更新、权限隔离、混合检索)
  • 2.13 简历 + 面试
  • 2.14 练习题

2.1 为什么需要 RAG

大模型的”知识墙”

第 1 章已讲:模型是”续写器”,知识冻结在训练时刻,且会幻觉。它有三堵墙:

  1. 知识时效墙:训练后发生的事它不知道。
  2. 私有知识墙:你的内部文档、数据库它没见过。
  3. 幻觉墙:不知道时它会编。

RAG 的解法

1
2
3
4
5
flowchart LR
A[用户问题] --> B[检索器 Retriever<br>从知识库召回相关片段]
B --> C[增强 Prompt<br>问题 + 检索到的片段]
C --> D[LLM 生成<br>基于片段回答并引用]
D --> E[答案 + 来源]

核心思想:回答前,先从”可更新的外部知识库”里检索出与问题最相关的片段,拼进上下文,再让模型基于这些片段回答。于是:

  • 知识可更新(改知识库即可,不用重训模型)。
  • 私有知识可用(把内部文档灌进库)。
  • 幻觉可抑制(给足证据 + 强制引用)。

三个关键子问题(全书主线)

  1. 怎么把文档变成可检索的形式(Embedding / 索引)→ 2.2~2.5
  2. 怎么召回最相关的片段(检索质量)→ 2.6~2.9
  3. 怎么保证回答正确且可溯源(生成与评估)→ 2.10~2.11

2.2 Embedding 原理与实战(TS 可运行)

什么是 Embedding

Embedding(嵌入) 是把一段文本映射成一个固定维度的稠密向量(如 1024 维),让语义相近的文本向量距离更近。

“猫在睡觉” 和 “猫咪睡着了” → 向量夹角很小。
“猫在睡觉” 和 “股票上涨了” → 向量夹角很大。

模型用向量内积 / 余弦相似度来衡量语义相似度,这就是语义检索的基础。

为什么不是”关键词匹配”

关键词(BM25)要求字面命中:”如何退款” 搜不到 “退货流程”(同义但无公共词)。Embedding 通过语义空间,能召回字面不同但语义相同的片段——但代价是对精确词、数字、专有名词不敏感。两者互补(这正是混合检索的来源)。

TS 可运行示例

1
2
npm i openai dotenv
# 用 OpenAI 兼容的 embedding 端点(DeepSeek 无 embedding,可用 Qwen/OpenAI 或本地)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// embed.ts —— 统一 embedding 封装
import OpenAI from "openai";
import "dotenv/config";

export const client = new OpenAI({
baseURL: process.env.EMBED_BASE_URL ?? process.env.BASE_URL ?? "https://api.openai.com/v1",
apiKey: process.env.EMBED_API_KEY ?? process.env.API_KEY ?? "EMPTY",
});

export async function embed(texts: string[]): Promise<number[][]> {
const resp = await client.embeddings.create({
model: process.env.EMBED_MODEL ?? "text-embedding-3-small",
input: texts,
});
return resp.data.map((d) => d.embedding);
}

// 无 Key 本地跑:Ollama 支持 embedding 模型,如 nomic-embed-text
// EMBED_BASE_URL=http://localhost:11434/v1 EMBED_MODEL=nomic-embed-text

验证语义:把三条句子丢进去,算两两余弦相似度,观察”语义相近”的分值高于”语义无关”。


2.3 向量检索原理:距离度量、相似度与 TopK

常见相似度度量

度量 公式(直觉) 适用
余弦相似度 向量夹角余弦,忽略长度 文本语义(最常用)
内积(点积) 大小 + 方向的乘积 归一化后等价余弦,向量库优化常用
欧氏距离 空间直线距离 数值/图像特征
曼哈顿距离 各维绝对差之和 稀疏/高维特定场景

对文本,余弦相似度最主流:cos(A,B) = A·B / (|A|·|B|)。

TopK 检索过程

  1. 把查询文本 embedding 成向量 q。
  2. 与库中每个向量算相似度(暴力遍历,或走索引加速)。
  3. 取相似度最高的 TopK 个片段返回。

为什么需要索引(向量库的价值)

库里几百万向量时暴力遍历太慢。向量数据库用 HNSW / IVF 等近似最近邻(ANN)索引,用”空间换时间”加速,代价是轻微召回损失。选型就是在”速度 / 内存 / 召回精度”间取舍(2.5)。

关键认知:TopK 太小漏召回,太大稀释上下文

  • TopK 过小 → 关键片段没进上下文。
  • TopK 过大 → 噪声多、上下文被稀释、成本涨。
  • 工程做法:先取较大候选集(如 50),再靠 Rerank 精排(2.9)压缩到 3~5 条。

2.4 文档处理:解析、清洗、分块与版本更新

检索质量的上限取决于索引质量。文档处理管线决定喂给 embedding 的”原文”长什么样。

管线四步

  1. 解析:PDF/Word/HTML → 纯文本。TS 用 pdf-parse、docx、cheerio 等。注意:扫描件要先 OCR。
  2. 清洗:去页眉页脚、图表残留、乱码、统一编码。
  3. 分块(Chunking):把长文切成合适大小的片段,这是最影响检索质量的环节。
  4. 索引 + 版本更新:向量入库,并维护元数据(来源、章节、更新时间)。

分块策略(机制与取舍)

策略 做法 适用
固定大小 按 Token/字符硬切 简单但易切断语义
递归分隔 按标题→段落→句子的优先级切 通用默认
语义分块 按语义边界(段落/标题)切 结构化文档
父子分块 大块检索、小块喂给模型 检索召回 + 上下文精准兼顾

关键认知:分块太小 → 片段缺上下文、召回碎片化;分块太大 → 检索不精准、上下文浪费。没有万能块大小,要按文档结构 + 任务实测调。

TS 可运行示例(递归分隔 + 元数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// chunk.ts
export interface Chunk {
id: string;
text: string;
meta: Record<string, unknown>; // 来源、标题、序号等
}

export function recursiveChunk(
text: string,
opts: { size?: number; overlap?: number } = {},
): string[] {
const { size = 500, overlap = 50 } = opts;
const seps = ["\n\n", "\n", "。", ";", " "];
// 简化实现:按段落再按 size 切,保留 overlap
const out: string[] = [];
let cur = "";
for (const para of text.split(/\n+/)) {
if (cur.length + para.length > size && cur) {
out.push(cur);
cur = cur.slice(-overlap); // overlap 保留上文
}
cur += para;
}
if (cur) out.push(cur);
return out;
}

说明:工程上建议直接用 LangChain.js 的 RecursiveCharacterTextSplitter(第 5 章讲框架时引入),这里先让你理解原理。


2.5 向量数据库选型:Qdrant / Chroma / Milvus

维度 Qdrant Chroma Milvus
定位 轻量高性能,社区友好 极简、内嵌式 大规模企业级分布式
部署 本地/Docker/云 内存/本地文件 集群
混合检索 ✅ 内置稀疏向量 ⚠️ 需配合 ✅ 强
过滤 强(payload 过滤) 基础 强
适用 中小规模、快速上线、本书主推 原型、学习 百万级+、生产集群

选型建议:个人/中小项目无脑用 Qdrant(Docker 一条命令起,原生支持稀疏向量做混合检索);大规模生产再上 Milvus。记住:先跑通,再扩容。


2.6 混合检索原理:BM25、向量召回与融合排序

为什么单一检索不够

  • 向量检索:懂语义,但对精确词/编号/专有名词不敏感。
  • BM25(稀疏/关键词检索):精确命中强,但不懂同义改写。

业务问题常同时包含两者:”退货运费是 KB1021 规则?”——“运费”需语义,”KB1021”需精确。混合检索两路并行再融合。

BM25 机制

BM25 是经典词频加权排序:

  • 词在文档里出现越多越相关(TF)。
  • 词越稀有(在库中越少见)越重要(IDF 逆文档频率)。
  • 对文档长度归一化(长文档中同样的 TF 相关度更低)。

融合排序(RRF,Reciprocal Rank Fusion)

两路结果合并最常用的方法 RRF:

1
score(d) = Σ  1 / (k + rank_i(d))

对每个文档在每路检索里的排名取倒数相加(k 通常 60)。排名高 → 分高,无需统一分数尺度,鲁棒。


2.7 实战:用 Qdrant 实现 BM25 + 混合检索(TS 全可运行)

起服务

1
2
docker run -p 6333:6333 -v $(pwd)/qdrant_storage:/qdrant/storage qdrant/qdrant
npm i @qdrant/js-client-rest

索引 + 混合检索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// hybrid.ts
import { QdrantClient } from "@qdrant/js-client-rest";
import { embed } from "./embed";

const qdrant = new QdrantClient({ url: "http://localhost:6333" });
const COLLECTION = "kb";

export async function ensureCollection(dim: number) {
const exists = await qdrant.collectionExists(COLLECTION);
if (!exists.result) {
await qdrant.createCollection(COLLECTION, {
vectors: { size: dim, distance: "Cosine" },
sparse_vectors: { text: {} }, // 开启稀疏向量,支持 BM25
});
}
}

export async function indexText(id: string, text: string) {
const [dense] = await embed([text]);
await qdrant.upsert(COLLECTION, {
points: [{
id,
vector: { text: dense, // 稠密
sparse_text: sparseBm25(text) }, // 稀疏:用关键词权重,简化用词频
payload: { text },
}],
});
}

// 简化 BM25 稀疏向量:term -> weight(工程上可接 tantivy/BM25 索引)
function sparseBm25(text: string) {
const freq = new Map<string, number>();
for (const w of text.toLowerCase().split(/\W+/)) freq.set(w, (freq.get(w) ?? 0) + 1);
return {
indices: [...freq.keys()].map((k) => hashIdx(k)),
values: [...freq.values()],
};
}

export async function hybridSearch(query: string, limit = 5) {
const [q] = await embed([query]);
return qdrant.query(COLLECTION, {
prefetch: [
{ query: q, using: "text", limit }, // 向量路
{ query: sparseBm25(query), using: "sparse_text", limit }, // BM25 路
],
query: { fusion: "rrf" }, // RRF 融合
limit,
});
}

这段让你看到混合检索的真实形态:两路召回 + RRF 融合。生产上 BM25 建议接专用索引(如 Tantivy/ES),Qdrant 的稀疏向量是轻量替代。


2.8 查询优化:Query Rewrite 与 Multi-Query

用户的原始问句往往不适合直接检索:

  • 太口语:”那个啥,我上次买的那个订单退款咋回事”
  • 指代不清:”它里面的规则是啥”(”它”指什么?)
  • 子问题多:”对比 A 和 B 的退款与保修政策”

三种主流优化

方法 做法 解决
Query Rewrite 让 LLM 把问句改写成更适合检索的形式(补全指代、去口语) 指代、口语
HyDE 先让 LLM 生成一段”假设答案”,用答案去检索 缩小语义鸿沟
Multi-Query 让 LLM 生成多个角度的问句,分别检索再合并 单一 query 召回不全

Multi-Query 示例(TS)

1
2
3
4
5
6
7
8
9
10
// multi_query.ts
import { chat } from "./llm";

export async function expandQueries(question: string): Promise<string[]> {
const resp = await chat([
{ role: "system", content: "把问题改写成 3 个不同检索角度的子问题,只输出 JSON 数组。" },
{ role: "user", content: question },
], { temperature: 0.3 });
return JSON.parse(resp);
}

工程提示:Multi-Query 让召回率↑但噪声↑,通常要配 Rerank 精排(2.9)去噪。


2.9 Rerank 重排:精排与上下文压缩

为什么需要 Rerank

初检(向量/BM25)追求高召回(宁可多召回),返回 50 条。但真正喂给模型的只有 3~5 条。需要一个更精确的排序模型把最相关的挑出来——这就是 Rerank 精排。

机制:交叉编码器(Cross-Encoder)

Rerank 模型(如 BGE-Reranker)把”查询 + 候选文档”拼成一对输入完整模型,算一个相关性分数。相比 embedding 的双塔(Bi-Encoder,查询和文档各自编码再比相似度),Cross-Encoder 精度更高,但无法预先索引(只能实时对候选集跑),所以只能做”精排”而非”初检”。

1
初检(快、召回高)→ 候选 50 条 → Rerank(慢、精度高)→ Top 3~5 条进上下文

TS 可运行示例(用 Jina/Cohere 或本地 bge-reranker)

1
npm i jina-ai  # 或按 provider 接 rerank API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// rerank.ts
export interface RankedDoc { id: string; text: string; score: number }

export async function rerank(
query: string,
docs: { id: string; text: string }[],
topN = 5,
): Promise<RankedDoc[]> {
// 示意:接 Jina Reranker 或本地 bge-reranker API
// const resp = await fetch("https://api.jina.ai/v1/rerank", {...});
// 这里为可运行,用简单启发:按与 query 的字符重叠度打分
return docs
.map((d) => ({ ...d, score: overlapScore(query, d.text) }))
.sort((a, b) => b.score - a.score)
.slice(0, topN);
}

function overlapScore(q: string, doc: string): number {
const qw = new Set(q.toLowerCase().split(/\W+/));
const dw = doc.toLowerCase().split(/\W+/);
return dw.filter((w) => qw.has(w)).length / Math.sqrt(dw.length + 1);
}

⚠️ 示意代码用启发式打分为”可运行”,生产请接真实 Rerank 模型(BGE-Reranker 可本地部署)。


2.10 答案生成:来源引用与拒答机制

生成时要做三件事

  1. 引用来源:答案里标注依据来自哪些片段(片段 id / 文档标题 / 页码)。
  2. 只依据检索内容:system 明确”只基于给定资料回答,不得编造”。
  3. 拒答机制:检索不到足够证据时,明确说”资料库中无相关信息”,而不是硬答。

工程实现要点(TS 思路)

1
2
3
4
5
6
7
8
9
10
11
const answerPrompt = (question: string, context: string[]) => [
{
role: "system",
content: [
"你是知识库问答助手。只依据下方【资料】回答,不得使用外部知识。",
"若资料不足以回答,直接回复:资料库中没有相关信息。",
"回答末尾用 [来源: <文档标题>] 标注依据。",
].join("\n"),
},
{ role: "user", content: `【资料】\n${context.join("\n---\n")}\n\n【问题】\n${question}` },
];

关键认知:拒答不是”软弱”,而是可信度护栏。宁缺毋滥,宁可说不知道也不编造——这是企业级 RAG 的基本盘。


2.11 RAG 质量评估:Recall@K、MRR、Faithfulness(RAGAS)

不做评估的 RAG 是”拍脑袋”。评估分两类:

检索评估(离线,需标注)

指标 含义
Recall@K 前 K 个结果里包含多少真正相关的
MRR 第一个相关结果排在第几位(取倒数)
Precision@K 前 K 个结果里相关占比

生成评估(可用 LLM 打分:RAGAS)

指标 含义
Faithfulness 答案是否忠于检索到的资料(不编造)
Answer Relevance 答案是否切题
Context Relevance 检索到的上下文是否相关

TS 可运行:可用 ragas 的 Python 生态(多语言),或自己用 LLM-as-a-judge 实现 Faithfulness 打分。至少要有样例集 + 指标,别靠感觉上线。


2.12 实战:企业知识库(文档更新、权限隔离、混合检索)

把 2.2~2.11 串成一个可运行的最小企业知识库。

需求

  • 支持文档增量更新(改了一版,旧版本失效)。
  • 支持权限隔离(不同用户只能搜到自己有权限的文档)。
  • 支持混合检索 + Rerank + 引用 + 拒答。

架构

1
2
3
4
5
6
7
8
flowchart LR
Doc[文档上传] --> Ingest[解析+清洗+分块]
Ingest --> Vec[向量入库+payload]
Q[用户提问] --> Auth[权限过滤 payload]
Auth --> Hybrid[混合检索 RRF]
Hybrid --> Rank[Rerank 精排]
Rank --> Gen[LLM 生成+引用+拒答]
Gen --> Ans[答案+来源]

关键实现点(TS 片段)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 权限隔离:检索时带 payload filter
export async function searchWithAuth(query: string, userId: string, limit = 5) {
const [q] = await embed([query]);
return qdrant.query(COLLECTION, {
prefetch: [
{ query: q, using: "text", limit: 20,
filter: { must: [{ key: "allowed_users", match: { value: userId } }] } },
],
query: { fusion: "rrf" }, limit,
});
}

// 版本更新:同 docId 先删旧再插入新
export async function upsertDocument(docId: string, chunks: { text: string; seq: number }[]) {
await qdrant.delete(COLLECTION, { filter: { must: [{ key: "docId", match: { value: docId } }] } });
for (const [i, c] of chunks.entries()) {
const [v] = await embed([c.text]);
await qdrant.upsert(COLLECTION, {
points: [{ id: `${docId}-${c.seq}`, vector: { text: v, sparse_text: sparseBm25(c.text) },
payload: { docId, seq: c.seq, text: c.text, allowed_users: ["alice", "admin"] } }],
});
}
}

权限隔离的关键:不要把权限逻辑交给模型判断,要在检索层用 filter 硬过滤——模型可能”越权”,filter 不会。


2.13 简历怎么写 + 面试题

简历写法

✅ “搭建基于 Qdrant 的企业知识库 RAG 系统:实现递归分块、混合检索(向量+BM25+RRF)、Rerank 精排与文档版本更新,通过 RAGAS 评估将 Faithfulness 提升至 0.9+,支持多用户权限隔离。”

面试题速查

问题 答题要点(章节)
为什么需要 RAG? 时效/私有知识/幻觉三堵墙(2.1)
Embedding 和关键词检索区别? 语义 vs 字面,互补(2.2)
余弦相似度怎么算? 内积除模长(2.3)
分块大小怎么定? 无万能,按结构+实测,父子分块(2.4)
为什么用混合检索? 语义+精确互补,RRF 融合(2.6)
Rerank 和 embedding 区别? Cross vs Bi-Encoder,精度/速度取舍(2.9)
权限隔离怎么做? 检索层 payload filter 硬过滤,不靠模型(2.12)
怎么评估 RAG? 检索指标 Recall/MRR + 生成指标 RAGAS(2.11)

练习题

  1. 用 2.2 的 embed(),测三条句子的余弦相似度,验证语义相近。
  2. 写一个递归分块函数,观察不同 size/overlap 对切分效果的影响。
  3. 起 Qdrant,用 2.7 的 hybridSearch 对比”纯向量 vs 混合检索”在含专有名词 query 上的差异。
  4. 实现拒答逻辑,验证”资料不足时模型不说谎”。

第 3 章 MCP、Tool Calling 与能力连接

大模型只会”说话”,Agent 要”做事”。让模型调用外部工具(查数据库、调 API、操作文件)是 Agent 的能力之源。
本章先手写一个 Tool Calling 循环让你彻底搞懂机制,再深入 MCP(Model Context Protocol) 这个”工具的标准插座”,最后落成一个企业售后 MCP 实战(含权限隔离与幂等写入)。全 TypeScript 可运行。

本章地图:

  • 3.1 为什么 Agent 要”连工具”
  • 3.2 Tool Calling 原理:手写循环调用链(不依赖框架)
  • 3.3 MCP 架构:Host / Client / Server 与能力发现
  • 3.4 MCP Server 能力建模:Tools / Resources / Prompts
  • 3.5 MCP Host 实战:工具发现、Schema 映射与调用循环
  • 3.6 MCP 连接方式:stdio / Streamable HTTP / 远程
  • 3.7 实战:MCP 退款审核(人工确认、长任务)
  • 3.8 实战:企业售后 MCP(上)演示与结构拆解
  • 3.9 实战:企业售后 MCP(中)权限隔离与幂等写入
  • 3.10 实战:企业售后 MCP(下)长任务、MCP Apps 与 Agent Host
  • 3.11 简历 + 面试
  • 3.12 练习题

3.1 为什么 Agent 要”连工具”

模型的能力上限 = 训练数据;但通过工具,它能实时接触真实世界:

工具 让模型能
数据库查询 访问实时私有数据(超越训练数据)
HTTP API 调第三方服务(天气、支付、物流)
代码执行 算数、文件处理、调用脚本(补数学/确定性短板)
文件系统 读写文档、生成产物

核心机制:不是”模型自己执行”,而是模型决定调用哪个工具、给出参数,由宿主程序执行,再把结果回填给模型。这就是 Tool Calling。


3.2 Tool Calling 原理:手写循环调用链(不依赖框架)

不急着上框架,先手写循环,理解本质。

机制:模型怎么”要求”调工具

  1. 模型 API 允许你声明一组 tools(JSON Schema 描述)。
  2. 模型收到问题后,如果判断需要工具,会返回 tool_calls(含工具名 + 参数 JSON),而不是直接给最终回答。
  3. 宿主执行工具,把结果作为 role: "tool" 消息回填。
  4. 模型基于工具结果继续生成,可能再调工具,直到给出最终答案。
1
2
用户 → [模型] → 请求调用 search()  → 宿主执行
宿主 → [tool 结果] → 模型 → 最终回答

TS 手写循环(可运行,不依赖 LangChain)

1
npm i openai dotenv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// tool_loop.ts —— 手写 Agent 循环
import OpenAI from "openai";
import "dotenv/config";

const client = new OpenAI({
baseURL: process.env.BASE_URL ?? "https://api.deepseek.com/v1",
apiKey: process.env.API_KEY ?? "EMPTY",
});

// 1) 声明工具(JSON Schema)
const TOOLS = [{
type: "function",
function: {
name: "get_weather",
description: "查询某城市天气",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
},
}];

// 2) 宿主实现工具
const registry: Record<string, (args: any) => Promise<string>> = {
async get_weather({ city }) { return `今天 ${city} 晴,25°C`; },
};

async function run(question: string) {
const messages: any[] = [{ role: "user", content: question }];
let rounds = 0;
while (rounds++ < 5) { // 预算:最多 5 轮,防死循环
const resp = await client.chat.completions.create({
model: process.env.MODEL ?? "deepseek-chat",
messages,
tools: TOOLS,
});
const msg = resp.choices[0].message;
if (msg.tool_calls?.length) {
// 3) 执行工具并回填
messages.push(msg as any);
for (const tc of msg.tool_calls) {
const result = await registry[tc.function.name as string](JSON.parse(tc.function.arguments));
messages.push({ role: "tool", tool_call_id: tc.id, content: result });
}
} else {
return msg.content; // 最终答案
}
}
return "超出执行预算";
}

console.log(await run("北京天气怎么样?"));

关键认知(面试常考):

  1. 工具结果必须 role:"tool" 且带 tool_call_id 回填,否则模型无法关联。
  2. 必须设置轮次预算(最多 N 轮),否则模型可能陷入无限调用。
  3. 工具描述写得好不好,直接决定模型会不会正确调用(提示词工程延伸到工具)。

3.3 MCP 架构:Host / Client / Server 与能力发现

手写循环每接一个新工具都要改代码。MCP 把这个过程标准化:工具可以”插拔”,模型宿主动态发现并调用。

1
2
3
4
5
6
flowchart LR
App[宿主应用<br>如 Claude Desktop / 你的 Agent]
App -->|MCP 客户端| C[MCP Client]
C <-->|stdio / HTTP| S[MCP Server 1<br>天气工具]
C <--> S2[MCP Server 2<br>数据库工具]
C <--> S3[MCP Server 3<br>文件工具]
角色 职责
Host 宿主应用:管理连接、把工具暴露给模型、执行调用
Client 与单个 Server 建立连接、发现能力、发调用请求
Server 提供能力:声明 Tools / Resources / Prompts,执行并返回结果

能力发现(Capability Discovery)

MCP 不是”预埋好工具”,而是运行时发现:Client 连上 Server 后,通过 tools/list 拿到工具列表(含 Schema),再映射给模型。新增能力无需改宿主代码——这是 MCP 最大的价值。


3.4 MCP Server 能力建模:Tools / Resources / Prompts

MCP 定义三类能力:

能力 是什么 类比
Tools 可执行的动作(函数),有输入 Schema 函数 / 工具
Resources 可读取的数据/文件(上下文资料) 文件 / 文档
Prompts 预设的提示模板 常用指令

Tools vs Resources(关键区分,面试常考)

Tools Resources
语义 “做一件事”(有副作用) “读一段数据”(无副作用)
触发 模型主动调用 宿主按需注入上下文
例子 refund() send_email() 文档、数据库记录、日志

一句话:Tools 是”动作”,Resources 是”资料”。


3.5 MCP Host 实战:工具发现、Schema 映射与调用循环(TS)

用官方 TS SDK 写一个真实 MCP 调用。

1
npm i @modelcontextprotocol/sdk openai

3.5.1 一个最小 Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// server.ts —— 用 TS SDK 声明一个天气 MCP Server
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "weather", version: "1.0.0" });

server.tool(
"get_weather",
"查询城市天气",
{ city: z.string().describe("城市名") },
async ({ city }) => ({ content: [{ type: "text", text: `今天 ${city} 晴,25°C` }] }),
);

const transport = new StdioServerTransport();
await server.connect(transport);
console.error("weather server running");

3.5.2 Host 侧:发现工具 + 映射 + 调用循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// host.ts —— 用 stdio 客户端连上 server,动态发现工具并驱动模型
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import OpenAI from "openai";
import "dotenv/config";

const transport = new StdioClientTransport({
command: "npx", args: ["tsx", "server.ts"], // 启动 server 进程
});
const client = new Client({ name: "host", version: "1.0.0" });
await client.connect(transport);

// 1) 动态发现工具(能力发现)
const { tools } = await client.listTools();
const openaiTools = tools.map((t) => ({
type: "function",
function: { name: t.name, description: t.description ?? "", parameters: t.inputSchema as any },
}));

// 2) 调用循环(与 3.2 同理,但工具来自 MCP 发现)
const llm = new OpenAI({ baseURL: process.env.BASE_URL, apiKey: process.env.API_KEY });
const messages: any[] = [{ role: "user", content: "北京天气?" }];
const resp = await llm.chat.completions.create({ model: process.env.MODEL, messages, tools: openaiTools });
const msg = resp.choices[0].message;
if (msg.tool_calls?.[0]) {
const tc = msg.tool_calls[0];
// 3) 通过 MCP 调用,而非本地 registry
const toolResp = await client.callTool({ name: tc.function.name, arguments: JSON.parse(tc.function.arguments) });
messages.push(msg as any, { role: "tool", tool_call_id: tc.id, content: JSON.stringify(toolResp.content) });
const final = await llm.chat.completions.create({ model: process.env.MODEL, messages });
console.log(final.choices[0].message.content);
}

这一步很关键:工具列表是运行时发现的(listTools()),调用是通过 MCP(callTool())而非硬编码——这就是”插拔”能力的本质。


3.6 MCP 连接方式:stdio / Streamable HTTP / 远程

传输 特点 适用
stdio 子进程 + 标准输入输出,同机 本地工具、CLI 集成(最简单)
Streamable HTTP HTTP + SSE,可跨机器 远程服务、Web 后端
SSE 旧式 SSE(已被 Streamable HTTP 取代) 兼容旧客户端

选型:本地开发用 stdio(上面示例就是);生产/跨进程用 Streamable HTTP(Server 起 HTTP 端点,Client 用 StreamableHTTPClientTransport)。

1
2
3
// HTTP Server 传输(生产场景)
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
// ... 把 transport 换成 HTTP 版,挂到 Express/Hono 路由

3.7 实战:MCP 退款审核(人工确认、长任务)

企业工具往往不能”全自动执行”——涉及钱、权限、长耗时。两个企业级模式:

人工确认(Human-in-the-Loop)

敏感操作(退款)需要暂停等人工批准,而非模型直接执行。两种实现:

  1. 工具内确认:工具执行时返回”需人工确认”状态,宿主进入等待。
  2. 审批通道:调用进入待审批队列,人工通过后才真正执行(第 7 章深入)。

长任务(Long-running)

耗时操作(生成报表)不能同步等。模式:

  1. 异步任务:工具立即返回 taskId,客户端轮询/回调拿结果。
  2. 进度事件:工具通过 MCP 的通知/进度机制回传进度。

这些在 3.8~3.10 的售后案例中落地。


3.8~3.10 实战:企业售后 MCP(结构拆解 / 权限隔离与幂等写入 / 长任务与 MCP Apps)

3.8 项目演示与结构拆解

场景:企业售后机器人,接多个后端能力(订单查询、退款、工单、客服转接),通过一个 MCP Server 暴露给 Agent。

1
2
3
4
5
6
7
agents/                         
├─ server.ts MCP Server:声明订单/退款/工单等工具
├─ host.ts Agent Host:发现工具 + 驱动模型 + 调用
└─ services/ 后端业务逻辑(模拟)
├─ order.ts 查订单
├─ refund.ts 退款(含幂等 + 人工审核)
└─ ticket.ts 建工单

3.9 权限隔离与幂等写入(硬核工程点)

权限隔离:工具层做校验,不信任模型

每个工具调用前,校验”该用户/该 Agent 是否有权执行”。

1
2
3
4
5
server.tool("refund", "发起退款", { orderId: z.string(), userId: z.string() },
async ({ orderId, userId }) => {
if (!canRefund(userId)) return { content: [{ type: "text", text: "无退款权限" }], isError: true };
return refundService.create(orderId, userId);
});

原则:权限判断在工具内部强制,模型只是”提议”,最终执行权在受控代码里。

幂等写入:防止重复执行

同一退款被调用两次不能退两次。用幂等键 + 状态机:

1
2
3
4
5
6
7
8
9
// refund.ts —— 幂等:requestId 唯一,重复请求返回第一次结果
const refunds = new Map<string, { status: string }>();

export function createRefund(orderId: string, userId: string, requestId: string) {
if (refunds.has(requestId)) return { status: "duplicate", data: refunds.get(requestId) }; // 已处理过
const record = { status: "pending_approval" };
refunds.set(requestId, record);
return { status: "created", data: record };
}

为什么幂等重要:Agent 可能重试、超时重发、多次调用。没有幂等,一次退款会重复扣钱/重复写库。这是企业 MCP 的必修课。

3.10 长任务、MCP Apps 与 Agent Host

  • 长任务:refund 返回后进入”待审批”,用 pending_approval 状态 + 轮询/事件查询进度。
  • MCP Apps / Agent Host:宿主层面可以把多个 MCP Server 组合,形成一个”有状态、可审批、可恢复”的 Agent 运行时(这为第 5、7 章 LangGraph 的持久化执行做铺垫)。

3.11 简历怎么写 + 面试题

简历写法

✅ “基于 MCP 搭建企业售后 Agent:封装订单/退款/工单为标准化 MCP 工具,实现工具层权限隔离与幂等写入(退款重复调用不重复扣款),接入人工审批,支撑长任务异步执行。”

面试题速查

问题 答题要点(章节)
Tool Calling 流程是什么? 模型返回 tool_calls→宿主执行→tool 消息回填→循环(3.2)
为什么要有轮次预算? 防死循环/无限调用(3.2)
MCP 是什么? 工具标准插座,运行时发现(3.3)
Tools 和 Resources 区别? 动作 vs 资料(3.4)
stdio 和 HTTP 传输区别? 本地子进程 vs 跨进程远程(3.6)
权限隔离怎么做? 工具内校验,不信任模型提议(3.9)
幂等写入怎么实现? 幂等键 + 状态机(3.9)

练习题

  1. 用 3.2 手写循环接一个”计算器”工具,验证多轮工具调用。
  2. 用 TS SDK 跑通 3.5 的 stdio Server + Host。
  3. 给 refund 工具加幂等键,模拟两次相同请求,验证只执行一次。
  4. 思考:你的业务里哪些操作必须”人工审批”?为什么?

第 4 章 Agent 执行原理、任务规划与失败恢复

第 3 章让 Agent 能”调工具”。但一个真正的 Agent 还要能判断下一步做什么、在预算内收敛、失败后恢复。本章从机制层讲透 ReAct、任务规划、运行时与失败恢复,并落成一个 AI 编程 Agent 实战。全 TypeScript。

本章地图:

  • 4.1 从”模型”到”Agent”
  • 4.2 ReAct Agent:推理、行动、观察
  • 4.3 任务规划:从 ReAct 到 Plan-and-Execute
  • 4.4 Agent Runtime:运行状态、执行预算与终止条件
  • 4.5 结果校验:空结果、证据冲突与失败恢复
  • 4.6~4.8 实战:AI 编程 Agent(演示 / ReAct 与执行控制 / 失败恢复)
  • 4.9 简历 + 面试
  • 4.10 练习题

4.1 从”模型”到”Agent”

模型 Agent
输入 一条 Prompt 一个目标 + 可用工具 + 环境
行为 生成一段文本 循环:推理→行动→观察,直到完成
关键 一次生成 循环 + 状态 + 终止 + 恢复

Agent 的引擎仍是 LLM,但多了执行循环(agentic loop) 和 运行时(runtime)。本章就把这个循环和运行时讲透。


4.2 ReAct Agent:推理、行动、观察

ReAct = Reason(推理)+ Act(行动)。核心是让模型交替输出”思考”和”行动”,行动结果(观察)再喂回去。

1
2
3
4
5
Thought: 用户想知道北京天气,需要查询工具。
Action: get_weather("北京")
Observation: 晴 25°C
Thought: 已拿到天气,可以回答了。
Answer: 北京今天晴,25°C。

为什么有效:显式”思考”让模型在行动前推理,减少盲目调用;”观察”把外部结果纳入下一次决策。

与思维链(Chain-of-Thought)的区别

CoT ReAct
只有推理 ✅ ✅
能调用工具 ❌ ✅
能感知外部结果 ❌ ✅

一句话:ReAct = CoT(思考)+ 工具调用(行动)+ 观察闭环。

TS 实现(在 3.2 循环基础上加”思考”字段)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// react.ts —— 让模型输出思考 + 工具调用的结构化循环
import OpenAI from "openai";
import "dotenv/config";

const client = new OpenAI({ baseURL: process.env.BASE_URL, apiKey: process.env.API_KEY });
const registry = {
get_weather: async ({ city }: any) => `今天${city}晴,25°C`,
get_time: async () => `现在 ${new Date().toLocaleTimeString()}`,
};

const TOOLS = Object.entries(registry).map(([name, fn]) => ({
type: "function",
function: { name, description: "查询工具", parameters: { type: "object", properties: {}, required: [] } },
}));

export async function reactAgent(goal: string, maxRounds = 6) {
const messages: any[] = [{ role: "system", content: "逐步思考,用工具,最后给答案。" },
{ role: "user", content: goal }];
for (let round = 0; round < maxRounds; round++) {
const resp = await client.chat.completions.create({
model: process.env.MODEL, messages, tools: TOOLS,
});
const msg = resp.choices[0].message;
if (msg.tool_calls?.length) {
messages.push(msg as any);
for (const tc of msg.tool_calls) {
const r = await registry[tc.function.name as keyof typeof registry](JSON.parse(tc.function.arguments));
messages.push({ role: "tool", tool_call_id: tc.id, content: String(r) });
}
} else {
return msg.content; // 已给出最终答案
}
}
return "达到最大轮数,任务未完成";
}

生产上不必手写 ReAct,LangChain 的 createAgent 内置了(第 5 章)。这里手写是为了让你看清本质。


4.3 任务规划:从 ReAct 到 Plan-and-Execute

ReAct 的局限:走一步看一步

ReAct 没有全局规划,复杂任务会”只见树木不见森林”,可能绕路、重复、丢失目标。

Plan-and-Execute:先规划,再执行

1
2
3
用户: 调研竞品并生成报告
Plan: 1) 搜索竞品 2) 收集数据 3) 生成报告 4) 导出
执行: 逐步执行,每步可重新规划(Re-plan)
1
2
3
4
5
6
flowchart LR
Goal[目标] --> Planner[Planner<br>拆解成子任务计划]
Planner --> Exec[Executor<br>逐个执行子任务]
Exec --> Check{检查结果}
Check -->|需调整| Planner[重新规划]
Check -->|完成| Done[汇总输出]

何时用哪种(工程决策)

任务复杂度 推荐 原因
简单(单次工具调用) 直接 ReAct 快、省 token
多步但线性 Plan-and-Execute 有全局视角
复杂、会失败 Plan-and-Execute + Re-plan + 失败恢复 能自我纠正

关键认知:规划能力 = LLM 的推理能力 + 你给它的”规划提示”(如何拆解、如何验证每步)。规划器本身也要有”重规划”入口,不能一锤定音。


4.4 Agent Runtime:运行状态、执行预算与终止条件

ReAct 是”策略”,Runtime 是”保障”。真实 Agent 必须有:

1. 执行预算(Budget)

  • 轮数上限:最多循环 N 轮(防死循环,3.2 已见)。
  • Token 预算:限制输入/输出 token(防上下文爆炸、控成本)。
  • 时间预算:最长执行时长(防长任务失控)。

2. 终止条件(Termination)

  • 正常终止:模型给出最终答案(无 tool_calls)。
  • 预算耗尽:达轮数/token/时间上限。
  • 明确失败:连续多次无进展 / 关键步骤失败。
  • 人工接管:遇到高权限操作,暂停等人确认。

3. 运行状态(State)

Agent 需要保存:当前消息历史、已调用的工具与结果、中间产物、剩余预算。这个”状态”是 Agent 可恢复、可观测的基础(第 5 章 LangGraph 的 State,第 7 章持久化的核心)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// runtime.ts —— 一个最小 Runtime
export interface AgentState {
messages: any[];
toolCalls: number;
maxCalls: number;
startedAt: number;
maxSeconds: number;
status: "running" | "done" | "budget_exceeded" | "failed" | "awaiting_human";
}

export function canContinue(s: AgentState): boolean {
return s.status === "running"
&& s.toolCalls < s.maxCalls
&& (Date.now() - s.startedAt) < s.maxSeconds * 1000;
}

4.5 结果校验:空结果、证据冲突与失败恢复

Agent 工具结果不一定正确。必须校验 + 恢复。

常见失败与处理

失败类型 例子 处理
空结果 查库返回 0 行 换检索词 / 换工具 / 告知用户无数据
证据冲突 两个工具结果矛盾 让模型对比,明确”以哪个为准”或请求澄清
工具报错 API 超时、参数错 捕获错误 → 让模型重试 / 修正参数 / 换方案
无进展 连续几轮重复调用同一工具 强制终止并反馈

失败恢复机制

  1. 错误回填:把错误信息作为 tool 结果回填,让模型自我修正(不直接崩溃)。
  2. 重试策略:瞬时可重试(超时退避);确定性错误不重试直接换方案。
  3. 断路器:连续失败超过阈值,中断当前路径,走备选或人工。
1
2
3
4
5
6
7
// 失败恢复:工具抛错 → 回填错误 → 让模型修正
try {
const r = await registry[name](args);
messages.push({ role: "tool", tool_call_id: id, content: String(r) });
} catch (e: any) {
messages.push({ role: "tool", tool_call_id: id, content: `工具出错: ${e.message},请修正参数或换方案` });
}

关键认知:失败不是”崩溃”,而是可观察、可回填、可恢复的信号。好的 Agent 把错误当输入,而不是中断。


4.6~4.8 实战:AI 编程 Agent(TS)

把 4.2~4.5 串成一个”能改代码、能跑命令、能自我纠错”的最小 AI 编程 Agent。

4.6 需求与工具

  • 工具:read_file、write_file、run_command(执行 shell)。
  • 目标:让 Agent 读懂一个文件、做修改、运行验证。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// coding_tools.ts
import { exec } from "node:child_process";
import { promisify } from "node:util";
import { readFile, writeFile } from "node:fs/promises";
const run = promisify(exec);

export const codingTools = {
read_file: async ({ path }: any) => (await readFile(path, "utf-8")).slice(0, 2000),
write_file: async ({ path, content }: any) => { await writeFile(path, content); return "written"; },
run_command: async ({ cmd }: any) => {
try { const { stdout } = await run(cmd, { timeout: 10000 }); return stdout; }
catch (e: any) { return `执行失败: ${e.message}`; } // 错误作为观察回填
},
};

4.7 ReAct + 执行控制

把上面的 tools 接入 4.2 的 reactAgent,给足预算(轮数 + 命令超时),实现”读→改→跑→看结果→修正”。

1
2
3
const result = await reactAgent(
"阅读 src/config.ts,把端口改成 8080,然后运行 npm run build 验证",
);

4.8 失败恢复、人工确认与执行轨迹

  • 失败恢复:run_command 返回”执行失败”,Agent 看到后分析日志、修正再试。
  • 人工确认:写文件 / 跑部署这类”有副作用”操作,可设 confirm 标志暂停等人批准。
  • 执行轨迹(Trace):把每一步的 Thought/Action/Observation 记录下来,既是可观测(排障)也是简历里可讲的项目亮点。
1
2
// 轨迹记录
export const trace: { round: number; thought?: string; action: string; observation: string }[] = [];

4.9 简历怎么写 + 面试题

简历写法

✅ “实现基于 ReAct 的 AI 编程 Agent:支持读改文件、执行命令的循环控制,具备轮数/时间/命令超时三层预算与错误回填式失败恢复,记录 Thought/Action/Observation 执行轨迹用于可观测。”

面试题速查

问题 答题要点(章节)
ReAct 是什么? 推理+行动+观察闭环(4.2)
ReAct 和 CoT 区别? CoT 只推理,ReAct 能调工具感知外部(4.2)
为什么要任务规划? ReAct 无全局,Plan-and-Execute 有全局+可重规划(4.3)
Runtime 要有哪些保障? 预算(轮数/token/时间)+ 终止条件 + 状态(4.4)
工具失败怎么处理? 错误回填→模型修正,重试/断路器(4.5)
为什么 Agent 要可观测? 排障、安全、简历可讲(4.8)

练习题

  1. 用 4.2 的 reactAgent 接 4.6 的 codingTools,完成”改端口+构建”任务。
  2. 给 Agent 加”无进展检测”:连续 3 轮调用同一工具就终止。
  3. 模拟工具连续失败,验证断路器逻辑。

第 5 章 LangChain / LangGraph:核心能力、运行原理与源码解析

手写循环(第 4 章)让你懂了本质,但生产开发要用成熟框架。LangChain.js 与 LangGraph.js 是 TS 生态最主流的 Agent 框架。
本章不满足于”会用 API”,而是深入 createAgent 的源码执行循环、State 数据隔离、Middleware 机制,让你成为”能讲清原理、能改框架”的工程师。全 TypeScript。

本章地图:

  • 5.1 框架如何选型
  • 5.2 核心组件:模型、消息、工具与 Runnable
  • 5.3 createAgent 原理:执行循环与流式输出
  • 5.4 数据隔离:State 与 Runtime Context
  • 5.5 Middleware 原理:Hook、拦截与校验
  • 5.6 LangGraph 状态图执行模型
  • 5.7 LangChain × LangGraph 分层编排
  • 5.8~5.11 createAgent 源码解析
  • 5.12 LangChain 生态:LangSmith 与集成
  • 5.13 上下文工程在 LangChain 的落地
  • 5.14 简历 + 面试
  • 5.15 练习题

5.1 框架如何选型:LangChain / LangGraph / 自研

方案 优点 缺点 适用
手写循环 完全可控、零依赖 要自己处理状态/流/恢复/重试 学习、极简 demo
LangChain.js 组件丰富、Runnable 链 抽象多、重 快速搭建标准 RAG/Agent
LangGraph.js 显式状态图、可控、可持久化 学习曲线 生产级复杂 Agent(主推)
LangGraph + LangChain 组件 图 + 组件组合,最佳实践 概念多 本书主线

结论:简单用 LangChain 链,复杂/生产用 LangGraph,两者可组合。本书以 LangGraph 为主骨架 + LangChain 组件。


5.2 核心组件:模型、消息、工具与 Runnable

Runnable 接口(LangChain 的统一抽象)

几乎所有东西都是 Runnable:有 invoke()、stream()、batch()。这让组件可以自由组合成链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 核心 Runnable 组合:Prompt → Model → Parser
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";

const prompt = ChatPromptTemplate.fromMessages([
["system", "你是技术教练,简短回答"],
["human", "{question}"],
]);
const model = new ChatOpenAI({ model: "deepseek-chat", temperature: 0 });
const parser = new StringOutputParser();

const chain = prompt.pipe(model).pipe(parser); // Runnable 组合
console.log(await chain.invoke({ question: "什么是 RAG?" }));

关键:.pipe() 让 invoke 的输出自动成为下一步输入——这是”链”的本质。


5.3 createAgent 原理:执行循环与流式输出

LangChain 提供 createAgent,内置了我们在第 4 章手写的 ReAct 循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// create_agent.ts
import { createAgent } from "@langchain/langgraph/prebuilt";
import { ChatOpenAI } from "@langchain/openai";
import { tool } from "@langchain/core/tools";

const weather = tool(async ({ city }: { city: string }) => `今天${city}晴`, {
name: "get_weather", description: "查天气",
schema: { city: { type: "string" } },
});

const agent = await createAgent({
model: new ChatOpenAI({ model: "deepseek-chat" }),
tools: [weather],
});

// 流式输出:逐 token 观察(自回归的外在表现)
const stream = await agent.stream({ messages: [{ role: "user", content: "北京天气?" }] }, { streamMode: "updates" });
for await (const chunk of stream) console.dir(chunk, { depth: null });

createAgent 在内部做什么(机制)

它封装了:

  1. 消息循环:把模型输出、工具调用、工具结果串起来(第 3、4 章的手写逻辑)。
  2. 工具注册与执行:按 Schema 调用工具、回填。
  3. 流式:支持 token 级 / 节点级流式(streamMode)。
  4. 状态管理:消息历史作为 State 传递(5.4)。

5.4 数据隔离:State 与 Runtime Context

为什么需要 State

Agent 有”运行状态”:消息历史、中间变量、工具结果、用户信息。这些要在节点间传递,但不能全部塞进发给模型的上下文。

State 与”发给模型的上下文”分离

  • State:完整运行状态(含不发模型的内部变量)。
  • 发给模型的消息:只从 State 里取”该给模型看的部分”(如 user/assistant/tool 消息,过滤掉内部标记)。
1
2
3
4
5
6
7
import { Annotation } from "@langchain/langgraph";

const State = Annotation.Root({
messages: Annotation<any[]>({ reducer: (a, b) => a.concat(b) }), // 消息累积
userId: Annotation<string>,
internalNotes: Annotation<string>, // 只内部用,不进上下文
});

关键认知:数据隔离 = “完整状态”与”模型可见上下文”分离。内部数据(userId、internalNotes)不污染模型,模型只见该见的消息。这也是防 prompt 注入、控 token 的关键。


5.5 Middleware 原理:Hook、拦截与结果校验

LangChain 支持在 Agent 执行链上挂 Middleware(中间件),类似 Express 的中间件。

1
2
3
4
5
6
7
8
9
10
// middleware.ts —— 在每次模型调用前后 hook
const middleware = {
beforeModel: async ({ state }) => {
console.log("[beforeModel] 消息数:", state.messages.length);
// 可在这里做:注入上下文、校验、脱敏
},
afterTool: async ({ state }) => {
// 可在这里做:校验工具结果、记录审计日志、截断超长结果
},
};

Middleware 能做什么(工程价值)

Hook 用途
beforeModel 上下文注入、敏感信息脱敏、预算检查
afterTool 结果校验、超长截断、审计日志、成本统计
afterModel 输出格式校验、安全过滤
error 统一异常处理、失败恢复

关键:Middleware 是 Agent 的”AOP(面向切面)”——把横切关注点(日志、校验、安全、成本)从业务逻辑里抽出来。


5.6 LangGraph 状态图执行模型

LangGraph 的核心:把 Agent 建模成状态图(StateGraph)。

1
2
3
4
5
6
flowchart TD
Start([Start]) --> Agent[Agent 节点<br>模型+工具]
Agent --> Cond{是否调工具?}
Cond -->|是| Tools[工具节点]
Tools --> Agent
Cond -->|否| End([End])

三要素

  1. State:图共享的状态(5.4)。
  2. Node(节点):一个处理单元(Agent 调用、工具执行、人工审核)。
  3. Edge(边)+ 条件分支:定义流转;条件边根据 State 决定走哪条路。
1
2
3
4
5
6
7
8
9
10
11
12
13
// graph.ts
import { StateGraph, END, Annotation } from "@langchain/langgraph";

const State = Annotation.Root({ messages: Annotation<any[]>({ reducer: (a, b) => a.concat(b) }) });

const graph = new StateGraph(State)
.addNode("agent", agentNode)
.addNode("tools", toolsNode)
.addEdge("agent", "tools", { condition: (s) => (needsTool(s) ? "tools" : "end") })
.addConditionalEdges("agent", (s) => (needsTool(s) ? "tools" : END), ["tools", END])
.addEdge("tools", "agent")
.addEdge("agent", END)
.compile();

为什么 LangGraph 强:显式状态 + 显式图 = 可预测、可中断、可恢复、可测试。这是手写循环难以保证的生产级特性。


5.7 LangChain × LangGraph 分层编排

最佳实践:确定性流程用 LangChain 链,自主决策用 LangGraph Agent,两者组合。

1
2
3
4
flowchart LR
Q[用户请求] --> Router{Router Agent<br>分类}
Router -->|FAQ| FAQChain[LangChain 固定链<br>查库回答]
Router -->|复杂任务| Agent[LangGraph Agent<br>自主规划工具]

价值:简单任务走固定链(快、稳、省 token),复杂任务才动用 Agent——成本和稳定性的平衡。


5.8~5.11 createAgent 源码解析(进阶硬核)

这部分是”媲美并超越付费课”的核心。目标是让你能讲清 createAgent 内部到底怎么跑。

5.8 初始化与状态 Schema

createAgent 内部:

  1. 构建一个包含 messages 的 State(见 5.4)。
  2. 把传入的 tools 注册为图里的工具节点。
  3. 用 model.bindTools(tools) 让模型知道工具 Schema。
1
createAgent = 一个预编译好的 LangGraph 子图 + 工具注册 + 模型绑定

5.9 Middleware 与图编译

createAgent 允许传 middleware,在编译图时插入到模型调用前后(5.5 的 hook 点)。编译后得到一个可 invoke/stream 的可执行图。

5.10 模型调用与 Middleware 执行

每轮循环:

  1. beforeModel hook。
  2. 调用 model.invoke(messages)(可能返回 tool_calls)。
  3. afterModel hook。
  4. 若无 tool_calls → 结束,返回最终消息。
  5. 若有 → 进工具节点。

5.11 ToolNode 与工具执行循环

工具节点:

  1. 逐个执行 model 请求的工具。
  2. 把结果作为 role:"tool" 消息(带 tool_call_id)回填 State。
  3. 回到模型节点,继续循环(预算内)。

你看到的第 3、4 章手写逻辑,createAgent 就是把它工程化、状态化、可扩展化了。理解了原理,你就能:何时直接用 createAgent、何时自定义图、如何加 Middleware。


5.12 LangChain 生态:LangSmith 与集成

  • LangSmith:官方可观测/评估平台。看执行轨迹(trace)、跑评测、抓回归。生产 Agent 强烈建议接入。
  • 集成包:@langchain/openai、@langchain/qdrant、@langchain/community 等,官方维护各厂商/数据库适配。

工程建议:接入 LangSmith(或自建 trace)做可观测,否则 Agent 出问题无法定位。


5.13 上下文工程在 LangChain 的落地

把第 1 章”上下文工程”思想落到框架:

  • RAG 检索 → 用 retriever 把相关片段拼进 prompt(第 2 章)。
  • 消息裁剪 → trimMessages 只留最近 N 条。
  • 记忆 → 第 6 章的 checkpointer / store。
  • 中间件注入 → beforeModel 里动态注入上下文。

一句话:框架给你的是”编排能力”,装什么进上下文、装多少,仍是你的上下文工程决策。


5.14 简历怎么写 + 面试题

简历写法

✅ “基于 LangGraph 构建生产级 Agent:用 StateGraph 显式建模执行流,通过 Middleware 实现上下文注入、结果校验与审计日志,支持流式输出与失败恢复。”

面试题速查

问题 答题要点(章节)
Runnable 是什么? 统一 invoke/stream/batch 接口,可 pipe 组合(5.2)
createAgent 内部做了什么? 消息循环+工具注册+状态+流式,预编译子图(5.3/5.8)
State 和模型上下文区别? 完整状态 vs 可见部分,数据隔离(5.4)
Middleware 有什么用? AOP,横切日志/校验/安全/成本(5.5)
LangGraph 为什么适合生产? 显式状态图,可预测/中断/恢复/测试(5.6)
什么时候用链、什么时候用 Agent? 固定流程用链,自主决策用 Agent(5.7)
ToolNode 怎么工作? 执行工具→tool 消息回填→回模型节点(5.11)

练习题

  1. 用 5.2 搭一个”检索→生成”链。
  2. 用 5.6 的 StateGraph 自定义一个带人工审核节点的图。
  3. 给 createAgent 加 Middleware,统计每次调用的 token 成本。
  4. 读 createAgent 源码,画出它的内部执行循环图。

第 6 章 Agent Memory、会话恢复与 Agentic RAG

第 1 章讲过:模型每次只看到当前上下文。要让 Agent”记住过去、跨会话认识用户”,必须有记忆系统。
本章从”短期记忆→会话恢复→长期记忆→记忆提取/更新/召回”讲透 Memory 的机制,再深入 Agentic RAG(让 Agent 自己决定何时检索),最后落成一个 AI 面试教练实战。全 TypeScript。

本章地图:

  • 6.1 从聊天记录到可用记忆
  • 6.2 短期记忆与会话恢复:Thread 与 Checkpointer
  • 6.3 上下文压缩:裁剪、摘要与 Compaction
  • 6.4 长期记忆:Store、用户画像与跨会话保存
  • 6.5 记忆提取:该记住什么、何时写入
  • 6.6 记忆更新与遗忘:去重、冲突修正与删除
  • 6.7 记忆召回:如何选择本次需要的信息
  • 6.8 Agentic RAG:按需检索、多轮补查与证据判断
  • 6.9~6.11 实战:AI 面试教练
  • 6.12 简历 + 面试 + 练习

6.1 从聊天记录到可用记忆

“记忆”分两类,工程含义完全不同:

短期记忆 长期记忆
内容 当前会话的消息历史 跨会话的用户画像、偏好、事实
存哪 运行态 / Thread Store(向量库 / KV / 数据库)
目的 保持对话连贯 下次会话还记得你

注意:模型本身无记忆。所谓”记忆”= 你在上下文里回放 历史消息/画像。记忆系统的本质是”上下文工程的持久化层”。


6.2 短期记忆与会话恢复:Thread 与 Checkpointer

Thread(线程):会话 ID

每个会话一个 threadId,Agent 按 threadId 存取该会话的历史消息。

Checkpointer:可恢复的运行状态

LangGraph 的 Checkpointer 在每一步执行后保存 State,让 Agent 能:

  • 恢复:服务重启 / 中途中断后,从断点继续。
  • 分叉:回溯到任一步(第 7 章人工审核、失败恢复的基础)。
1
2
3
4
5
6
7
8
9
10
11
// checkpoint.ts
import { MemorySaver } from "@langchain/langgraph";
import { createAgent } from "@langchain/langgraph/prebuilt";

const checkpointer = new MemorySaver();
const agent = await createAgent({ model, tools, checkpointer });

// 按 thread 连续对话(短期记忆)
const config = { configurable: { thread_id: "user-42" } };
await agent.invoke({ messages: [{ role: "user", content: "我叫小明" }] }, config);
await agent.invoke({ messages: [{ role: "user", content: "我叫什么?" }] }, config); // 记得叫小明

生产环境用 PostgresSaver / RedisSaver 等持久化 checkpointer(替代内存版),服务重启不丢。


6.3 上下文压缩:消息裁剪、自动摘要与 Compaction

会话越长,历史越占 Context Budget。压缩是必须的。

三种策略

策略 做法 适用
消息裁剪 只留最近 N 条 简单、丢早期关键信息
自动摘要 定期把旧消息摘要成一句,替换原文 保留要义、控 token
Context Compaction 智能地”压缩历史但保留关键事实” 生产首选
1
2
3
4
5
6
7
// 摘要节点:把旧消息压缩成摘要
import { ChatOpenAI } from "@langchain/openai";

async function summarize(old: string): Promise<string> {
const llm = new ChatOpenAI({ model: "deepseek-chat" });
return llm.invoke(`把以下对话压缩成要点摘要(保留关键事实与用户偏好):\n${old}`);
}

关键认知:压缩不是简单截断,是信息密度优化——把”关键事实”保留进摘要,丢弃冗长过程。第 1 章”上下文工程”的具体应用。


6.4 长期记忆:Store、用户画像与跨会话保存

Store:长期记忆的存储

LangGraph 提供 Store(可配内存 / Redis / Postgres / 向量库),按 namespace 存结构化数据。

1
2
3
4
5
6
7
8
9
10
11
12
// store.ts
import { InMemoryStore } from "@langchain/langgraph";

const store = new InMemoryStore();

// 写用户画像
await store.put(["users", "u42"], "profile", {
name: "小明", level: "中级前端", goal: "转 AI 开发",
});

// 读
const { value } = await store.get(["users", "u42"], "profile");

用户画像

长期记忆的价值在于个性化:记住偏好、历史目标、已学进度,下次会话主动利用。


6.5 记忆提取:该记住什么、何时写入

记住什么(不是所有都记)

  • 值得记:用户偏好、关键事实、目标、历史决策。
  • 不记:一次性闲聊、临时指令、可推断的噪声。

何时写入(时机)

  • 显式:用户明确说”记住……”。
  • 被动:对话中识别到稳定偏好时(如”我一般用 Vue”)。
  • 批量:会话结束时总结并入库。
1
2
3
4
// 提取记忆:让模型从对话中抽"值得长期记住的事实"
const facts = await llm.invoke(
`从对话中提取值得长期记住的用户事实(偏好/目标/背景),输出 JSON 数组,无则空数组:\n${conversation}`,
);

关键认知:记忆提取 = 让 LLM 做”信息筛选”。”记什么”比”怎么存”更考验设计——记太多是噪声,记太少没价值。


6.6 记忆更新与遗忘:去重、冲突修正与删除

记忆是动态的,要能更新、纠错、删除。

操作 场景 实现
去重 同一事实反复写入 按”事实键”合并
冲突修正 用户改了偏好(”我现在用 React”) 新事实覆盖旧事实
删除 用户要求遗忘 / 隐私 删对应 store 记录
1
2
// 冲突修正:新偏好覆盖旧
await store.put(["users", "u42"], "framework", { value: "React" }); // 覆盖旧值

工程要点:给每条记忆加 updatedAt,冲突时以最新为准;提供”清除用户数据”能力(合规与隐私)。


6.7 记忆召回:如何选择本次需要的信息

记忆存了,但不是每次全塞进上下文(会爆 budget)。要”按需召回”:

  • 根据当前问题,检索相关记忆(向量相似度 / 关键词)。
  • 只把相关画像、相关历史拼进上下文。
1
当前问题 → 召回相关的长期记忆 → 拼进上下文 → 模型回答

记忆召回 = 长期记忆上的 RAG。这就和第 2 章打通了:记忆也是”可检索的知识”。


6.8 Agentic RAG:按需检索、多轮补查与证据判断

普通 RAG vs Agentic RAG

普通 RAG Agentic RAG
何时检索 每次固定先检索 Agent 自己决定是否检索
多轮 一次检索 根据不足可多轮补查
判断 无 判断检索结果够不够、要不要换查法

Agentic RAG 的流程

1
2
3
4
5
Agent: 这个问题需要查资料吗?
├─ 不需要 → 直接回答
└─ 需要 → 检索 → 判断结果
├─ 不够 → 改写查询 / 换工具再查(多轮)
└─ 够 → 基于证据回答(来源引用)

用 LangGraph 把”检索判断”做成 Agent 决策节点(第 5 章状态图的典型应用)。


6.9~6.11 实战:AI 面试教练(TS)

6.9 训练流程与会话恢复

  • 用户设定目标(如”练 JS 面试”)→ 存长期记忆。
  • 每次会话用 checkpointer 恢复,知道上次练到哪。

6.10 记忆提取与用户画像

  • 每轮对话后提取”薄弱项、已掌握项、偏好”→ 更新画像。
  • 下次提问基于画像出题(个性化)。

6.11 跨会话复测与 Agentic RAG

  • 新会话先召回画像 + 历史薄弱项。
  • 用 Agentic RAG:按当前问题检索”题库 + 用户历史表现”,决定出简单/难题、是否补查。
1
2
3
4
5
// 面试教练核心:召回画像 + 按需检索出题
const profile = (await store.get(["users", uid], "profile"))?.value;
const recalled = await recallMemories(profile, question); // 6.7
const isHard = profile.weakTopics.includes(questionTopic);
// 用 Agentic RAG 决定是否检索题库 / 调整难度

6.12 简历怎么写 + 面试题 + 练习题

简历写法

✅ “实现 Agent 记忆系统:用 Checkpointer 支持跨会话恢复,用 Store 维护用户画像,通过 LLM 记忆提取 + 冲突修正实现长期个性化,并基于 Agentic RAG 按需检索与多轮补查。”

面试题速查

问题 答题要点(章节)
短期记忆和长期记忆区别? 会话内消息 vs 跨会话画像(6.1)
Checkpointer 有什么用? 保存 State、断点恢复、分叉(6.2)
上下文压缩怎么做? 裁剪 / 摘要 / Compaction,保关键事实(6.3)
记忆提取该记什么? 偏好/目标/事实,去噪声(6.5)
记忆冲突怎么处理? 以最新为准,updatedAt(6.6)
记忆召回是什么? 长期记忆上的 RAG,按需拼上下文(6.7)
Agentic RAG 和普通 RAG 区别? Agent 自决定是否检索、多轮补查(6.8)

练习题

  1. 用 6.2 的 checkpointer 做跨消息记忆,验证记得上文。
  2. 实现 6.5 的记忆提取,观察”哪些被记为值得记”。
  3. 给面试教练加 Agentic RAG:问题简单就不检索,复杂才查题库。
  4. 设计”冲突修正”:用户改偏好后,验证新会话用新值。

第 7 章 企业 Agent 工作流:DDD、持久化执行与 Human-in-the-Loop

前几章讲”Agent 能做什么”,本章讲”Agent 在企业里怎么稳”。生产 Agent 需要:业务建模(DDD)、持久化执行(重启不丢)、人工审核(HITL)、失败恢复(局部重做)。
本章把这些企业级特性讲透并落到一个 AI 演示文稿 Agent 实战。全 TypeScript。

本章地图:

  • 7.1 为什么企业 Agent 需要”暂停、修改、继续”
  • 7.2 业务建模与 DDD:领域模型与审核规则
  • 7.3 LangGraph 持久化执行:服务重启后如何恢复
  • 7.4 人工审核:暂停、批准、修改与恢复
  • 7.5 失败恢复:部分成功与局部重做
  • 7.6~7.8 实战:AI 演示文稿 Agent
  • 7.9 简历 + 面试 + 练习

7.1 为什么企业 Agent 需要”暂停、修改、继续”

消费级 Agent 可以”一把梭”,企业级不行:

  • 涉及钱/权限/外发,必须人工确认。
  • 任务长(生成报告/文稿),中途可能重启、超时、中断。
  • 出错了要能从断点继续,而不是重头再来。

所以企业 Agent 的核心能力是:可暂停、可恢复、可人工介入、可局部重做。这正是 LangGraph 状态图 + Checkpointer 的用武之地(第 5、6 章基础)。


7.2 业务建模与 DDD:领域模型与审核规则

为什么需要 DDD(领域驱动设计)

Agent 不是”会聊天”就行,要听懂业务:订单、工单、审批、状态流转。DDD 帮你把业务建模成清晰的领域模型。

核心概念(面试常考)

概念 含义 例子
实体(Entity) 有唯一 ID、有状态的对象 订单 Order
值对象(Value Object) 无唯一性、描述性 金额、地址
聚合(Aggregate) 一组一致性边界 订单 + 订单行
领域服务 跨实体的业务逻辑 退款审核
状态机 实体的合法状态流转 订单:待支付→已支付→已发货

审核规则(业务规则)

Agent 的每个”动作”都要过业务规则:谁能退、金额上限、是否需要审批。

1
2
3
4
5
6
7
8
// 领域模型 + 状态机
export type OrderStatus = "pending" | "paid" | "shipped" | "refunded";

export const refundRules = {
canRefund: (order: { status: OrderStatus; amount: number; days: number }) =>
order.status === "paid" && order.days <= 7, // 7 天内可退
needsApproval: (amount: number) => amount > 1000, // 大额需审批
};

关键:业务规则是确定性代码,不是让模型”判断”。模型提议动作,规则代码裁决——安全第一。


7.3 LangGraph 持久化执行:服务重启后如何恢复

持久化 = Checkpointer 落盘

第 6 章的内存 Checkpointer 换成数据库持久化(Postgres/Redis),State 每一步都存:

1
2
3
4
5
6
7
8
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";

const checkpointer = await PostgresSaver.fromConnString(process.env.DATABASE_URL!);
const graph = ...compile({ checkpointer });

// 服务重启后,用同一 thread_id 恢复继续
const config = { configurable: { thread_id: "order-1024" } };
await graph.invoke(updateMsg, config); // 从上次断点继续,而不是重头

恢复的意义

  • 长任务中断 → 从断点续跑,不重复劳动。
  • 可审计 → 每一步都有迹可查。
  • 可回放 → 出问题能定位到具体步骤。

7.4 人工审核:暂停、批准、修改与恢复(HITL)

HITL(Human-in-the-Loop)流程

1
2
3
4
5
6
7
flowchart TD
A[Agent 生成方案] --> P{需人工审核?}
P -->|是| Wait[暂停<br>Checkpointer 保存状态]
Wait --> Human[人工审核<br>批准 / 修改 / 拒绝]
Human -->|批准| Resume[继续执行]
Human -->|修改| Edit[应用修改] --> Resume
Human -->|拒绝| Stop[终止/改方案]

LangGraph 的 interrupt(暂停)

LangGraph 提供 interrupt(),在节点里暂停,等人供给数据后继续:

1
2
3
4
5
6
7
8
9
10
11
import { interrupt } from "@langchain/langgraph";

async function approvalNode(state: any) {
// 暂停,等人工决定
const decision = await interrupt({
type: "approval",
proposal: state.draft, // 给人工看的东西
});
// 人工返回 decision 后继续执行
return { status: decision.approved ? "approved" : "rejected", feedback: decision.feedback };
}

关键认知:interrupt = 在状态图某点”冻结”,等待外部输入再继续。这是企业 Agent 相对消费级的决定性能力。


7.5 失败恢复:部分成功与局部重做

问题:一个长流程中间某步失败

  • 全量重做:浪费、慢、可能重复副作用(如重复发送)。
  • 局部重做:只重做失败的那一步及其依赖,已成功的保留。

实现

  1. 每步把结果写进 State(持久化)。
  2. 失败时定位到失败节点,仅重跑该节点 + 依赖它的下游。
  3. 用 Checkpointer 回到失败前状态,注入修正后继续。

这也是”幂等”(第 3 章)的意义:重做时不会重复扣款/重复写库。


7.6~7.8 实战:AI 演示文稿 Agent(TS)

7.6 需求

用户给主题 → Agent 生成大纲 → 人工审核大纲 → 生成 PPT(pptx)→ 失败续做。

7.7 大纲审核与持久化执行

1
2
3
4
5
6
7
8
9
10
// 状态机:generating → awaiting_approval → approved → building → done
const graph = new StateGraph(State)
.addNode("generate_outline", async (s) => ({ outline: await llm(s.topic) }))
.addNode("human_approval", async (s) => {
const d = await interrupt({ action: "approve_outline", outline: s.outline });
return { outline: d.feedback ?? s.outline, status: d.approved ? "approved" : "revise" };
})
.addNode("build_pptx", buildPptx)
.addConditionalEdges("human_approval", (s) => (s.status === "approved" ? "build_pptx" : "generate_outline"))
.compile({ checkpointer }); // 持久化

7.8 失败续做、局部修改与导出

  • 失败续做:build_pptx 失败 → 从断点恢复,只重跑 build 节点。
  • 局部修改:人工说”第二页图表改一下” → 只重做受影响节点。
  • 导出:最终生成 .pptx 文件(可用 pptxgenjs)。
1
npm i pptxgenjs

这个案例把本章所有点串起来:DDD(审核规则)、持久化(checkpointer)、HITL(interrupt)、局部重做(断点续跑)。


7.9 简历怎么写 + 面试题 + 练习题

简历写法

✅ “构建企业级 Agent 工作流:用 DDD 建模订单/审核领域与状态机,基于 LangGraph Checkpointer 实现持久化执行与失败局部重做,通过 interrupt 实现人工审核(批准/修改/拒绝),支撑长任务断点续跑。”

面试题速查

问题 答题要点(章节)
企业 Agent 需要哪些特性? 暂停/恢复/人工介入/局部重做(7.1)
DDD 在 Agent 里的作用? 业务建模,规则确定性代码裁决(7.2)
服务重启怎么恢复? Checkpointer 落库,thread_id 续跑(7.3)
HITL 怎么实现? interrupt 冻结状态,人工输入后继续(7.4)
失败恢复怎么做? 只重做失败节点+依赖,幂等保证(7.5)

练习题

  1. 用 interrupt 实现”生成→人工批准→继续”的最小图。
  2. 把 6.2 的 checkpointer 换成 PostgresSaver,模拟服务重启恢复。
  3. 给退款加规则:大额需审批(interrupt),验证流程。

第 8 章 复杂任务 Agent:Skills、Workspace 与多 Agent 协作

单个 Agent 处理复杂任务(长篇创作、多源调研、持续迭代)会撞到上下文墙和单点能力墙。本章讲透 Agent Harness、Workspace、Skills,以及 Router / Handoff / Supervisor / Subagent 的选型与结果治理,最后落到一个多 Agent 剧情游戏实战。全 TypeScript。

本章地图:

  • 8.1 复杂任务为什么需要多 Agent
  • 8.2 Agent Harness:为什么复杂任务需要它
  • 8.3 Agent Workspace:文件化上下文
  • 8.4 Agent Skills:把方法封装成可复用能力
  • 8.5 多 Agent 协作模式:Router / Handoff / Supervisor / Subagent 怎么选
  • 8.6 多 Agent 结果治理:委派、产物契约与 Reviewer 返工
  • 8.7~8.10 实战:多 Agent 剧情游戏
  • 8.11 简历 + 面试 + 练习

8.1 复杂任务为什么需要多 Agent

单个 Agent 的瓶颈:

  1. 上下文墙:长任务(写一本剧情、调研 10 个来源)上下文会爆。
  2. 单点能力:一个模型/一套提示,难以同时擅长”策划 + 写作 + 校验”。
  3. 无分工:没有”专注的专家”,一个 Agent 什么都干 → 容易乱。

多 Agent 思路:拆成多个有明确职责的 Agent,各自专注,通过协作完成任务。这是”分而治之”在 Agent 架构里的体现。


8.2 Agent Harness:为什么复杂任务需要它

Harness(约束框架):让 Agent 在”有结构、有约束、可观测”的框架里运行,而不是自由放飞。

1
2
3
4
5
flowchart TD
Task[复杂任务] --> Harness[Agent Harness<br>工作流/预算/校验/观测]
Harness --> W[Workspace<br>文件化上下文]
Harness --> S[Skills<br>按需能力]
Harness --> M[多 Agent<br>分工协作]

Harness 提供:执行预算、校验门、审计日志、断点恢复、能力按需加载。它把”一次性自由调用”变成”受控的工程流程”。

你正在使用的这个助手环境本身就是一个 Harness 的例子:它给 Agent 分配工具、工作区、技能和校验,让复杂任务可靠完成。


8.3 Agent Workspace:文件化上下文

为什么需要 Workspace

复杂任务的”中间产物”(大纲、章节、调研笔记)不该只存在 Agent 的上下文里(会爆、会丢)。写成文件,需要时再读回。

文件化上下文的价值

内存上下文 文件化 Workspace
容量 有限、易爆 几乎无限
持久 会话结束丢 落盘可复用
协作 难共享 多 Agent 可读写同一工作区
可审计 难 每个文件可查
1
2
3
4
5
6
7
8
9
// workspace.ts —— 简单文件化工作区
import { mkdir, writeFile, readFile } from "node:fs/promises";

export class Workspace {
constructor(private dir: string) {}
async init() { await mkdir(this.dir, { recursive: true }); }
async write(name: string, content: string) { await writeFile(`${this.dir}/${name}`, content); }
async read(name: string) { return readFile(`${this.dir}/${name}`, "utf-8"); }
}

关键认知:把”长任务的中间状态”落盘成文件,让 Agent 摆脱上下文墙——这是复杂任务的可扩展关键。


8.4 Agent Skills:把方法封装成可复用能力

什么是 Skill

Skill = 一段”可复用的方法/流程”,封装成按需加载的能力(类似函数,但通常含提示词 + 工具 + 流程)。例如”如何设计剧情”、”如何做竞品分析”。

和 Tool / MCP 的区别

Tool Skill
是什么 单一可执行动作 一整套”方法 + 流程”
粒度 细(查天气) 粗(设计剧情)
触发 模型按需调 按需加载进上下文

一句话:Tool 是”手”,Skill 是”手艺”。Skill 把”怎么做一件事”的知识沉淀下来复用。


8.5 多 Agent 协作模式:Router / Handoff / Supervisor / Subagent 怎么选

这是多 Agent 架构的核心决策题。四种主流模式:

模式 结构 适合 特点
Router 一个路由器按意图分发到固定 Agent 意图分明的任务(客服分类) 简单、快、Agent 间无来回
Handoff A 做完交给 B(接力) 流水线任务 串行、单向
Supervisor 一个主管 Agent 调度多个 Worker 复杂、需动态分配 主管决策、灵活、可控
Subagent 主 Agent 委派子任务给子 Agent 并行子任务 主 Agent 汇总、子 Agent 独立
1
2
3
4
5
6
7
8
9
10
flowchart TD
subgraph Router
R[Router] --> A1[AgentA] & A2[AgentB]
end
subgraph Supervisor
S[Supervisor] --> W1[Worker1] & W2[Worker2]
end
subgraph Subagent
M[Main] --> Sub1[Sub1] & Sub2[Sub2]
end

怎么选(决策)

  1. 意图明确、任务固定 → Router。
  2. 线性流水线、每步明确 → Handoff。
  3. 复杂、需动态分工、要控制权 → Supervisor。
  4. 可拆成独立子任务、要并行 → Subagent。

经验法则:能用 Router 别用 Supervisor——越简单的编排越稳、越省 token、越好测。


8.6 多 Agent 结果治理:委派、产物契约与 Reviewer 返工闭环

多 Agent 最大的坑是结果不可控、格式不一致、质量无把关。治理三件套:

1. 任务委派(Delegation)契约

委派给子 Agent 时,明确:目标、输入、输出 Schema(产物契约)、验收标准、截止/预算。

1
2
3
4
5
6
7
8
interface SubtaskSpec {
id: string;
goal: string;
inputPath: string; // 从工作区读
outputPath: string; // 写到工作区
outputSchema: object; // 产物契约
maxRounds: number;
}

2. 产物契约(Artifact Contract)

每个子 Agent 的产物必须符合约定 Schema(JSON 校验 / 字段检查),不合格就打回。

3. Reviewer 返工闭环

多 Agent 架构里应有校验者(Reviewer):检查子 Agent 产物是否达标,不合格→返工。

1
2
3
委派 → 子 Agent 产出 → Reviewer 校验
├─ 通过 → 采纳
└─ 不通过 → 带反馈返工(最多 N 轮)

关键:没有 Review 的多 Agent = 失控。Reviewer 返工闭环是多 Agent 质量的基本盘。


8.7~8.10 实战:多 Agent 剧情游戏(TS)

8.7 需求与演示

用户给主题 → 多个 Agent 协作生成一段互动剧情游戏(策划 → 写作 → 校验 → 构建)。

8.8 Workspace、Skills 与主 Agent

  • Workspace:把大纲、章节、剧本写入文件(8.3)。
  • Skills:封装”剧情设计方法”(三幕结构、角色弧线、分支逻辑)为 Skill(8.4)。
  • 主 Agent:用 Supervisor/Subagent 模式调度。

8.9 Subagent 任务委派

1
2
3
4
5
6
7
8
9
10
11
12
// 委派:策划 Agent 产出大纲,写作用到文件
const outline = await delegate({
goal: "设计三幕互动剧情大纲",
inputPath: "workspace/prompt.md",
outputPath: "workspace/outline.json",
outputSchema: { acts: [{ title: "string", nodes: ["string"] }] },
skills: ["story_design"], // 按需加载 Skill
});

// 校验 + 返工
const review = await reviewerCheck(outline);
if (!review.pass) { await delegate(reviseSpec(outline, review.feedback)); }

8.10 一致性审核、返工与构建

  • 一致性:校验前后角色名、剧情线是否一致(Reviewer 职责)。
  • 返工:不一致 → 带具体反馈打回对应子 Agent。
  • 构建:把最终剧本转成可交互的游戏数据(JSON),交付。

这个案例演示了多 Agent 的完整形态:Harness(预算/校验)+ Workspace(文件化)+ Skills(方法沉淀)+ Subagent(分工)+ Reviewer(返工闭环)。


8.11 简历怎么写 + 面试题 + 练习题

简历写法

✅ “设计多 Agent 协作架构:基于 Agent Harness 实现 Workspace 文件化上下文与 Skills 按需加载,采用 Supervisor/Subagent 分工,通过产物契约与 Reviewer 返工闭环保证输出质量与一致性。”

面试题速查

问题 答题要点(章节)
为什么需要多 Agent? 上下文墙 + 单点能力墙 + 无分工(8.1)
Harness 是什么? 受控框架:预算/校验/观测/恢复(8.2)
Workspace 为什么重要? 文件化上下文,突破上下文墙、可协作(8.3)
Skill 和 Tool 区别? 手 vs 手艺,方法+流程(8.4)
Router/Supervisor/Subagent 怎么选? 意图固定用 Router,复杂动态用 Supervisor,独立子任务用 Subagent(8.5)
多 Agent 怎么保证质量? 委派契约 + 产物 Schema + Reviewer 返工闭环(8.6)

练习题

  1. 用 8.3 的 Workspace 把长任务中间产物落盘。
  2. 用 8.5 模式对比:同一任务分别用 Router 和 Supervisor 设计,分析差异。
  3. 实现一个 Reviewer:校验子 Agent 产出是否满足 Schema,不合格返工。

全书小结

恭喜你学完 8 章。你已走完:大模型原理 → RAG → MCP → Agent 执行 → LangGraph → Memory → 企业工作流 → 多 Agent 的完整闭环。现在你能:

  • 讲透”为什么这样设计”,经得起面试追问。
  • 独立交付可上线的 RAG / Agent / 多 Agent 应用。
  • 用工程化语言写进简历、复盘项目。

下一步建议:挑一个真实业务,用 LangGraph + Qdrant + MCP + Checkpointer 从零做一个端到端项目,把 8 章的能力串起来。祝顺利!