基座模型的应用:从 Prompt 到 Agent 系统
核心理念
大模型是概率生成器,不是知识数据库。应用层的核心工作是:把人类意图翻译成模型最容易理解的形式,把模型输出约束到工程系统能消费的格式。
前置知识
在讨论具体应用之前,先厘清几个贯穿全文的基础概念。
语言模型的概率生成本质
大模型在做的事情只有一件:给定已有的 token 序列 \(x_1, x_2, \ldots, x_t\),预测下一个 token \(x_{t+1}\) 的概率分布:
这不是"理解",而是"续写"。模型根据训练时见过的海量文本,学习了"什么样的文字后面最可能跟什么样的文字"。这意味着:
- 输出是概率性的:同一个 prompt 生成的结果可以不同(temperature > 0 时)
- 没有事实数据库:模型"知道"的东西是参数中编码的统计规律,不是显式的知识条目
- 上下文决定一切:模型输出完全由输入的 token 序列决定,给什么上下文就生成什么分布
Transformer 与注意力机制
当前主流大模型都基于 Transformer 架构。其核心是自注意力机制(Self-Attention):让序列中的每个 token 都能"看到"其他所有 token,并根据相关性分配不同的注意力权重。
graph LR
A[输入 token 序列] --> B[计算 Q, K, V 矩阵]
B --> C[注意力权重]
C --> D[加权求和 V → 输出]
D --> E[每个 token 获得全局上下文表示]
注意力权重的计算公式:
直觉:\(Q\)(Query)是"我在找什么",\(K\)(Key)是"我有什么",\(V\)(Value)是"我的内容"。\(QK^T\) 算出每对 token 之间的相关性分数,softmax 归一化后用这些分数对 \(V\) 做加权求和。
工程含义:序列越长,注意力矩阵越大(\(n \times n\)),计算量与序列长度的平方成正比。这是长上下文成本高的根本原因。
Token 与分词
Token 是模型处理文本的最小单位,由 tokenizer 将文本切分而成。Token 化方式直接影响模型表现和使用成本:
| 语言 | 典型切分 | 大致比例 |
|---|---|---|
| 英文 | 单词或子词 | 1 token ≈ 4 字符 ≈ 0.75 单词 |
| 中文 | 汉字或词组 | 1 token ≈ 1-2 个汉字 |
| 代码 | 关键字、符号、缩进 | 格式化代码比压缩代码消耗更多 token |
特殊符号(数学公式、emoji、非拉丁文字)可能被拆成多个 token。所有 API 的计费、窗口大小、速率限制都按 token 计算。
上下文窗口与 Prompt Caching
上下文窗口是模型一次能处理的 token 总量。
窗口不是越大越好:
- 成本:128K 窗口的推理成本远高于 4K 窗口(注意力计算量与序列长度平方成正比)
- 延迟:窗口越大,首 token 延迟(TTFT)越高
- 精度:存在 "Lost in the Middle" 现象——模型对窗口中间内容的注意力显著低于首尾(Liu et al., 2023)
Prompt Caching 是降低长上下文成本的关键手段。主流 API(Anthropic、OpenAI)支持对重复前缀进行缓存:如果多轮对话的 System Prompt 和历史前缀不变,缓存命中后输入 token 的成本可降低 50-90%。
工程意义:把稳定的、不常变化的内容放在上下文前面(System Prompt、工具定义、RAG 参考文档),把频繁变化的内容(用户新消息)放在后面,最大化缓存命中率。
注意力稀释
注意力稀释(attention dilution或context degradation)指早期的上下文虽然还在窗口内,但因为被后面大量新内容"稀释",模型对它的注意力已经大幅下降。
具体表现:
- 对话第 3 轮说的约束条件,到第 15 轮时模型可能已经"忘了"
- RAG 检索的参考文档被后续对话内容"挤"到了注意力的边缘
- System Prompt 中的规则在长对话中逐渐被忽略
这是所有 Agent 系统和长对话应用必须面对的问题,后续章节会讨论具体的工程对策。
Prompt 工程:从意图到指令
前置概念
- Prompt:发送给模型的完整输入,包括 System Prompt、对话历史、用户消息等
- In-context learning:模型从上下文中的例子自动提取模式的能力,无需更新参数
- Temperature:控制输出随机性的参数。0 表示确定性输出(每次相同),1 表示高随机性
核心目标:最小化意图不确定性
Prompt 写得模糊,概率分布就分散到多个方向上;写得精准,概率就集中到你想要的那条路径上。
一个例子,注意别被疾旋鼬看见
模糊 prompt:"帮我修一下代码"
→ 模型需要猜测:什么语言?什么错误?什么功能?概率分散
精准 prompt:"你是一个资深Python工程师,请帮我debug这段报'连接超时'的代码,可能的原因有网络问题或疾旋鼬啃坏了网线,请给出排查步骤和解决方案"
→ 模型只需要定位具体问题范围,概率集中
Prompt 的本质不是"哄模型",而是把你的意图翻译成模型最容易理解的形式。好的 Prompt 工程师做的事情和好的产品经理一样——把模糊的需求变成精确的规格说明。
三种策略
三种策略解决不同层面的问题:
graph TD
A[Prompt 策略] --> B[System Prompt]
A --> C[Few shot]
A --> D[Chain of Thought]
B -->|解决| B1[角色和规则问题]
C -->|解决| C1[格式对齐问题]
D -->|解决| D1[复杂推理问题]
System Prompt — 定义行为边界
System Prompt 在整个对话期间持续生效,设定模型的行为边界。关键在于约束而非指令——你不是告诉模型"做什么",而是告诉它"在什么框架下做":
一个例子,注意别被疾旋鼬看见
你是一个网络安全应急响应专家,负责分析系统异常事件。所有回答必须遵守以下框架: 1. 必须严格区分事实描述与推测分析 2. 涉及攻击路径的分析必须标注置信度(高/中/低) 3. 所有处置建议需包含可操作命令和回滚方案
当前已知约束:日志中发现“疾旋鼬”签名,该实体行为模式未载入知识库。

好的 System Prompt 应该像一份岗位说明书,而不是一份任务清单。
Few-shot — 用例子对齐格式
给模型看几个输入-输出的例子,比写一堆规则更有效。模型的 in-context learning 能力让它能从上下文中的例子自动提取模式:
一个例子,注意别被疾旋鼬看见
示例:
输入:系统出现“疾旋鼬进程占用CPU 90%”告警 → 紧急
输入:用户反馈疾旋鼬偶尔在日志中留下爪印图案 → 低危
输入:监控显示疾旋鼬相关进程每小时唤醒一次 → 观察
输入:疾旋鼬彻底消失,系统完全正常 → 正常
输入:疾旋鼬修改了配置文件但未破坏功能 →
关键:(1) 例子要覆盖边界情况;(2) 格式要和实际输入一致;(3) 3-5 个例子通常就够,太多反而浪费窗口。
Chain-of-Thought(CoT)— 分解复杂推理
让模型把思考过程写出来,而不是直接给结论。数学、逻辑、代码等需要多步推理的任务上效果显著:
一个例子,注意别被疾旋鼬看见
问题:一只疾旋鼬在服务器集群中捣乱,它的行为会影响错误日志数量。已知节点A每分钟因疾旋鼬产生3个错误日志,节点B每分钟产生2个错误日志,节点C每分钟自动修复1个错误日志。若当前集群错误日志总数为0,问多少分钟后错误日志总数达到100个?
让我们一步步思考:
-
净错误日志增加速率 = 3 + 2 - 1 = 4 个/分钟
-
达到100个所需时间 = 100 / 4 = 25 分钟
答案:25分钟
CoT 有效的根本原因:中间步骤可以纠错。直接生成最终答案时,模型的"推理"全在内部隐状态中完成,一步错全盘错。写出中间步骤后,每一步都可以被检查和修正,相当于把一个高维概率问题分解成多个低维概率问题。
Prompt 设计的工程原则
| 原则 | 说明 | 反例 |
|---|---|---|
| 约束优于指令 | 告诉模型边界比告诉它步骤更有效 | "先分析再总结" → "回答限 200 字,必须引用法条" |
| 结构化优于自然语言 | 用 JSON/XML/Markdown 格式化输入 | 一段话描述需求 → 用列表逐条列出 |
| 具体优于抽象 | 给出具体的例子和格式要求 | "写得好一点" → "参考这个格式:..." |
| 否定不如肯定 | 告诉模型"做什么"比"不做什么"更有效 | "不要写得太长" → "限 100 字以内" |
输出约束与幻觉防控
前置概念
- 幻觉(Hallucination):模型生成的内容看起来合理但与事实不符
- Structured Output:强制模型按指定格式(如 JSON Schema)输出
- Grounding:将模型输出锚定到真实数据源(RAG、API 返回值等)
幻觉的三种类型
大模型不是在"检索事实",而是在"预测最可能的下一个 token"。当模型对某个事实没有足够强的记忆时,它会根据语言模式"编造"一个看起来合理的答案。
| 类型 | 定义 | 例子 |
|---|---|---|
| 事实性幻觉 | 编造不存在的事实 | "爱因斯坦在 1921 年获得了诺贝尔化学奖" |
| 忠实性幻觉 | 回答偏离了给定的上下文 | RAG 检索到了正确文档但回答时"跑偏"了 |
| 逻辑性幻觉 | 推理过程中出现逻辑错误 | 数学证明中某一步推导错误 |
五层防线
幻觉无法完全消除,但可以逐层压缩其发生概率:
graph TD
A[用户输入] --> B[第一层:RAG]
B --> C[第二层:Function Calling]
C --> D[第三层:Structured Output]
D --> E[第四层:Prompt 约束]
E --> F[第五层:多轮验证]
F --> G[输出]
B -.->|用事实约束生成| B1[检索真实文档塞入上下文]
C -.->|用实时数据替代记忆| C1[调用 API 获取真实数据]
D -.->|约束输出格式| D1[JSON Schema 强制校验]
E -.->|明确告知边界| E1["'不确定就回答不确定'"]
F -.->|交叉检查| F1[用另一模型验证输出]
第一层:RAG — 用事实约束生成
把真实资料塞进上下文,让模型基于事实回答。RAG 不能完全消除幻觉——模型仍可能"忽略"检索到的文档而自行编造。所以需要配合 Prompt 约束:"请严格基于以下参考资料回答,如果资料中没有相关信息,请说'我无法从提供的资料中找到答案'"。
第二层:Function Calling — 用真实数据替代模型记忆
让模型调用真实接口获取数据,而不是凭"记忆"回答。详见第四章。
第三层:Structured Output — 约束输出格式
格式越固定,模型"自由发挥"的空间越小。详见下一节。
第四层:Prompt 约束 — 明确告知边界
在 Prompt 中明确要求:"如果不确定就回答'我不确定'"。虽然不是 100% 管用,但能显著降低编造概率。
第五层:多轮验证 — 交叉检查
用另一个模型或同一模型的不同采样来验证输出:
Structured Output
下游代码要解析模型输出,格式不确定就没法自动化。实现方式有两种:
方式一:Prompt 规定格式
问题:模型有时会"跑偏",输出不合法的 JSON(多余逗号、缺少引号等)。
方式二:JSON Schema 约束(推荐)
主流 API 都支持 response_format 参数,传入 JSON Schema,模型会被强制按 Schema 输出,格式 100% 合法:
{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"city": {"type": "string"}
},
"required": ["name", "age", "city"]
}
进阶用法:信息提取(简历解析)、分类(enum 约束类别)、多步推理(chain_of_thought + answer 两个字段)。
这是怎么做到的?
在模型推理侧通过约束解码技术实现。具体来说,当指定response_format参数时,系统会将JSON Schema转换为一套可执行的文法规则或有限状态机(通常是通过前缀树或确定性有限自动机来实现,将JSON Schema编译为这些结构,以便在生成每个Token时进行高效校验),在模型生成文本的每个步骤中实时验证和约束下一个token的选择。这意味着模型在输出时,不是先生成任意文本再解析,而是每一步都被限制只能生成符合Schema结构的合法token。例如,当Schema要求一个对象时,模型必须以花括号开始;当要求字符串时,模型必须生成引号并正确转义内容;当有枚举约束时,模型只能从枚举值中选择。这种机制确保最终的输出完全符合指定的格式,并且100%是合法的JSON。对于进阶用法如信息提取,模型在理解文本内容的同时,被强制将信息填入预设的结构化字段中;对于多步推理,Schema可以设计为包含chain_of_thought和answer两个字段,从而引导模型先输出推理过程再给出答案。整个过程是在解码阶段即时完成的,因此模型能够同时兼顾内容准确性和格式规范性。
RAG:检索增强生成
前置概念
- Embedding:将文本映射为高维向量的模型,语义相近的文本在向量空间中距离近
- 向量空间:Embedding 输出的高维数学空间,每个维度代表某种语义特征
- 余弦相似度:衡量两个向量方向一致性的指标,值域 [-1, 1],越接近 1 越相似
- ANN(Approximate Nearest Neighbor):近似最近邻搜索,在高维空间中快速找到最相似的向量
Embedding 的原理
Embedding 把文本转换为高维向量,语义相近的文本在向量空间中距离近:
"苹果公司发布新手机" → [0.2, -0.5, 0.8, ...]
"Apple launches new iPhone" → [0.3, -0.4, 0.7, ...]
"今天天气真好" → [-0.6, 0.1, -0.3, ...]
前两个向量距离近(语义相似),和第三个距离远(语义不同)。RAG 的检索不是关键词匹配,而是算向量距离——"苹果公司发布新手机" 和 "Apple launches new iPhone" 用词完全不同但能搜到,因为语义一样。
向量数据库
向量数据库的核心能力是快速做 ANN 检索:
| 向量数据库 | 特点 | 适用场景 |
|---|---|---|
| Milvus | 开源、高性能、支持分布式 | 大规模生产环境 |
| Pinecone | 全托管、易用 | 快速原型、中小规模 |
| Weaviate | 开源、支持混合搜索 | 需要关键词 + 向量混合检索 |
| Chroma | 轻量级、嵌入式 | 本地开发、小规模实验 |
四种检索策略
graph LR
A[用户问题] --> B{检索策略}
B --> C[向量检索<br/>抓语义相似]
B --> D[关键词检索<br/>抓精确匹配]
C --> E[RRF 融合]
D --> E
E --> F[粗排 top-50]
F --> G[Cross-Encoder 精排]
G --> H[最终 top-5]
H --> I[发给模型生成答案]
策略一:混合检索(Hybrid Search)
向量检索抓语义相似的,关键词检索抓精确匹配的(如产品编号、人名),两者用 RRF(Reciprocal Rank Fusion)合并:
其中 \(k\) 通常取 60,\(\text{rank}_i(d)\) 是文档 \(d\) 在第 \(i\) 种检索方式中的排名。
策略二:Chunk 大小优化
| Chunk 大小 | 优点 | 缺点 |
|---|---|---|
| 太小(<256 token) | 检索精准 | 丢上下文,回答不完整 |
| 适中(512-1024 token) | 平衡精准度和上下文 | 需要调参 |
| 太大(>2048 token) | 上下文完整 | 检索不精准,浪费窗口 |
实践建议:512-1024 token,带 50-100 token 重叠。
策略三:HyDE(Hypothetical Document Embedding)
先让模型生成一个"假答案",用假答案的 Embedding 去检索。为什么有效?因为假答案和真实文档的语义比问题本身更接近——问题是疑问句,文档是陈述句,直接用问题检索会有语义鸿沟。
策略四:Rerank(重排序)
先用向量检索召回 top-50 候选,再用 Cross-Encoder 精排到 top-5。向量检索用 Bi-Encoder(问题和文档分别编码),速度快但精度有限。Cross-Encoder 把问题和文档一起编码,精度高但速度慢。两阶段结合,取长补短。
完整 RAG Pipeline
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_anthropic import ChatAnthropic
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain.chains import RetrievalQA
# 1. 文档分块
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=100,
separators=["\n\n", "\n", "。", ",", " "] # 中文优化的分隔符
)
chunks = splitter.split_documents(documents)
# 2. 向量化 + 存储
embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-large-zh-v1.5")
vectorstore = Chroma.from_documents(chunks, embeddings, persist_directory="./chroma_db")
# 3. 检索 + 生成
llm = ChatAnthropic(model="claude-sonnet-4-20250514")
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
retriever=vectorstore.as_retriever(search_kwargs={"k": 5}),
chain_type="stuff"
)
result = qa_chain.invoke("疾旋鼬的关键问题是什么?")
实际生产中有三个关键细节值得展开。
Embedding 选择:语言决定了模型
Embedding 模型不是通用的——一个在英文上表现优秀的模型,中文效果可能很差。根本原因在于训练语料:模型只在见过的语言上有好的语义表示能力。
| 场景 | 推荐模型 | 维度 | 特点 |
|---|---|---|---|
| 中文为主 | BAAI/bge-large-zh-v1.5 |
1024 | 中文 MTEB 榜单领先,对中文短句和长文都有好的表示 |
| 中文为主(轻量) | moka-ai/m3e-base |
768 | 比 bge-large 小,推理快,效果略低但够用 |
| 英文为主 | text-embedding-3-small |
1536 | OpenAI 提供,性价比高,支持 Matryoshka(可截断维度降成本) |
| 多语言混合 | BAAI/bge-m3 |
1024 | 支持 100+ 语言,中英混合文档首选 |
| 大规模生产 | Cohere embed-v3 |
1024 | 支持检索优化和压缩,API 调用无需自建推理 |
选型决策树:
文档语言?
├── 纯中文 → bge-large-zh(效果最好)或 m3e-base(要快就用这个)
├── 纯英文 → text-embedding-3-small(便宜、好用)
├── 中英混合 → bge-m3(一个模型搞定)
└── 对延迟敏感 → 用 API 服务(Cohere/OpenAI),别自己部署
一个常见的坑:用 OpenAI 的 text-embedding-ada-002 处理中文,检索效果会明显不如 bge-large-zh。原因是 ada-002 的训练语料以英文为主,中文语义空间的分辨率不够。选错 Embedding 模型,后面所有优化都白费——向量本身就歪了,检索再怎么调参也没用。
分块策略:切法比切多大更重要
RecursiveCharacterTextSplitter 的核心设计是按语义边界递归切分,而不是按字符数硬切。它的工作方式:
# 分隔符的优先级从高到低
separators = ["\n\n", "\n", "。", ",", " "]
# 1. 先尝试用 "\n\n"(段落)切分
# 2. 如果切出来的 chunk 还是太大,用 "\n"(行)再切
# 3. 还是太大,用 "。"(句号)再切
# 4. 依次降级,直到每个 chunk 不超过 chunk_size
为什么这比硬切好?硬切会在句子中间断开,把一个完整的语义单元切成两半,导致检索到的 chunk 语义不完整,模型拿到残缺的上下文就容易答错。按语义边界切,每个 chunk 都是一个相对完整的语义单元。
可是代价是什么呢?
计算复杂度显著增加、分割结果的不确定性、对格式特殊文本的脆弱性、内存与状态管理负担、配置敏感性与调试复杂度
中文场景的关键配置:
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # 约 500-1000 个中文字符
chunk_overlap=100, # 重叠区域,防止语义在边界处断裂
separators=[
"\n\n", # 段落分隔(最高优先级)
"\n", # 换行
"。", # 句号——中文最自然的语义边界
";", # 分号——次级语义边界
",", # 逗号——最后手段
" " # 空格——中文基本用不到
],
length_function=len # 中文用字符数计算,不用 tokenizer
)
chunk_overlap=100 的作用:如果一个关键信息恰好落在两个 chunk 的边界上,没有重叠的话它只属于一个 chunk,检索时可能漏掉。有了重叠,边界处的信息在两个 chunk 里都有,检索命中率更高。overlap 一般设为 chunk_size 的 10%-15%。
进阶方案——语义分块(Semantic Chunking):不按固定分隔符切,而是用 Embedding 计算相邻句子的语义相似度,相似度骤降的地方就是最佳切分点。效果比递归切分好,但计算成本更高,适合对检索质量要求极高的场景。
检索后处理:检索到不等于能用
向量数据库返回 top-k 个 chunk,但这些 chunk 不能直接塞给模型。常见的问题和对策:
问题一:语义重复。 同一个段落被切成两个 chunk,两个都被检索到了,浪费窗口空间。
def deduplicate_chunks(chunks, threshold=0.92):
"""基于余弦相似度去重"""
unique = [chunks[0]]
for chunk in chunks[1:]:
is_dup = False
for u in unique:
sim = cosine_similarity(chunk.embedding, u.embedding)
if sim > threshold:
is_dup = True
break
if not is_dup:
unique.append(chunk)
return unique
问题二:相关性不足。 向量检索是"最相似"而不是"足够相似"——即使文档库里没有相关内容,它也会返回 k 个结果。需要设一个相似度阈值过滤:
retriever = vectorstore.as_retriever(
search_kwargs={
"k": 10, # 先多召回一些
"score_threshold": 0.7 # 过滤掉相似度低于 0.7 的
}
)
问题三:排序不准。 向量检索用的是 Bi-Encoder(问题和文档分别编码),速度快但精度有限。对于高要求场景,召回后用 Cross-Encoder 重排序:
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")
# 先召回 top-20,再精排到 top-5
candidates = vectorstore.similarity_search(query, k=20)
pairs = [[query, doc.page_content] for doc in candidates]
scores = reranker.predict(pairs)
# 按精排分数重新排序
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
final_chunks = [doc for doc, score in ranked[:5]]
Cross-Encoder重排序
Cross-Encoder 重排序是检索系统中的一个精细排序阶段。其核心操作是:将查询(Query)和一个候选文档(Document)同时输入到一个经过微调的、结构更复杂的预训练模型(如BERT)中,让模型对二者进行深度交互式编码。具体而言:
-
联合编码:模型将查询和文档拼接(如[CLS] 查询 [SEP] 文档 [SEP]),通过自注意力机制进行全交互,精确捕捉词语、短语间的语义匹配、同义替换和逻辑关系。
-
精细打分:模型基于这个深度交互后的整体表示,直接输出一个相关度分数(通常是一个回归或分类分数),而非基于独立向量的余弦相似度。
-
重排序:用此分数对所有初步召回(例如通过BM25或双塔模型得到)的Top K个候选文档进行重新精确排序,将最相关的结果提升至顶部。
它牺牲了双塔模型的预先计算和检索速度,换取了交互式模型的更高精度,是“召回-排序”流水线中,用更高计算成本换取最终效果提升的关键一步。
完整的后处理 Pipeline:
工具调用:Function Calling 与 MCP
前置概念
- Function Calling:模型生成结构化的函数调用请求,由外部执行层实际调用
- MCP(Model Context Protocol):Anthropic 提出的开放协议,标准化模型与工具的对接方式
- 工具白名单:执行层只允许调用已注册的工具,防止模型编造工具
Function Calling 机制
Function Calling 让模型能"调用工具"。模型不直接回答,而是生成一个函数调用请求:
执行层收到请求后,调用真实的天气 API,把结果返回给模型,模型再基于真实数据生成回答。核心价值:让模型的操作基于实时、真实的数据,而不是凭"记忆"。
MCP 协议
MCP 解决的是工具对接的标准化问题。没有 MCP 时,每接一个新工具就要写一套对接代码,换模型又得重写。有了 MCP,工具变成标准化的"插件":
from mcp.server import Server
from mcp.types import Tool, TextContent
server = Server("weather")
@server.tool()
async def get_weather(city: str) -> list[TextContent]:
"""查询指定城市的天气"""
result = await weather_api.query(city)
return [TextContent(type="text", text=f"{city}今天{result.temp}°C,{result.condition}")]
MCP 的实际生态:
| MCP Server | 功能 | 使用方式 |
|---|---|---|
| Filesystem | 文件读写、目录操作 | Claude Code 内置 |
| GitHub | PR/Issue/代码操作 | gh CLI 封装 |
| Database | SQL 查询 | 连接 PostgreSQL/SQLite |
| Puppeteer | 浏览器自动化 | 网页操作、截图 |
| 自建 Server | 任意业务逻辑 | 按 MCP 协议实现 |
Function Calling vs RAG
| 维度 | Function Calling | RAG |
|---|---|---|
| 数据来源 | 实时 API、数据库 | 离线文档、知识库 |
| 数据类型 | 结构化、实时 | 非结构化、离线 |
| 典型场景 | 天气查询、数据库操作 | 产品手册、内部文档 |
| 输出确定性 | 高(数据来自真实接口) | 中(模型仍可能"忽略"文档) |
| 延迟 | 低(API 调用快) | 中(检索 + 生成) |
实际项目中经常两者结合:RAG 提供知识背景,Function Calling 提供实时数据和执行能力。
Agent 系统架构
前置概念
- Agent:能自主决策、调用工具、循环执行直到完成目标的系统
- ReAct(Reasoning + Acting):一种 Agent 范式,交替进行推理(思考下一步)和行动(调用工具)
- Agent Loop:Agent 的核心循环——观察 → 思考 → 行动 → 观察结果 → 判断是否完成
- Tool Use:模型生成结构化工具调用请求的能力
Agent 的运行机制
Agent = 思维链 + Function Calling + 循环。其本质是一个循环执行的决策系统:
graph TD
A[用户输入] --> B[观察当前状态]
B --> C[思考:规划下一步]
C --> D[决定行动]
D --> E{需要调用工具?}
E -->|是| F[执行工具调用]
F --> G[观察结果]
G --> H{目标达成?}
H -->|否| C
H -->|是| I[输出最终结果]
E -->|否| I
具体例子:用户说"帮我查一下北京明天的天气,如果会下雨就帮团队发个邮件提醒带伞"。
Agent 执行过程:
- 规划:需要两个步骤——查天气、发邮件
- 执行步骤 1:调用
get_weather(city="北京", date="tomorrow")→ 返回"有雨" - 判断:会下雨,需要发邮件
- 执行步骤 2:调用
send_email(to="team@company.com", subject="明天有雨,记得带伞") - 判断:任务完成
Agent 和普通 LLM 应用的核心区别:模型自己决定下一步做什么,不需要人逐步指挥。
以 LangChain 的 ReAct Agent 为例:
from langchain.agents import AgentExecutor, create_react_agent
from langchain_anthropic import ChatAnthropic
from langchain.tools import Tool
tools = [
Tool(name="search", func=search_web, description="搜索网页"),
Tool(name="calculate", func=calculator, description="数学计算"),
]
llm = ChatAnthropic(model="claude-sonnet-4-20250514")
agent = create_react_agent(llm, tools, prompt_template)
executor = AgentExecutor(agent=agent, tools=tools, max_iterations=10, verbose=True)
result = executor.invoke({"input": "北京明天的气温比上海高多少?"})
max_iterations=10 是关键的安全阀——防止 Agent 无限循环。实际生产中还需要设置 max_execution_time(超时)和 handle_parsing_errors(解析失败时的容错)。
关于Agent容灾
LangChain Agent 的容灾机制通过分层防御策略实现,核心是在不牺牲灵活性的前提下确保系统鲁棒性。
1. 硬性执行控制:通过 max_iterations 和 max_execution_time 设置绝对边界,防止无限循环与资源耗尽,这是最基础的安全阀。
2. 软性错误恢复:handle_parsing_errors 等参数启用后,框架会捕获输出格式错误、工具异常等,并尝试引导 Agent 重新生成合规输出或执行降级操作,而非直接崩溃。
3. 工具层隔离与防护:每个工具函数内部实现独立的异常处理与输入验证,确保单点故障被封装,并以结构化错误信息返回给 Agent,使其能进行后续决策。
4. 状态可观测与用户接管:通过记录中间步骤和设置人工确认点,系统提供了执行过程的可审计性,并在关键决策或多次失败时允许人工干预,实现可控的优雅降级。
多 Agent 架构
单个 Agent 能力有限,复杂任务需要多个 Agent 协作:
graph TD
M[主 Agent<br/>调度/规划] --> S[搜索 Agent<br/>信息检索]
M --> C[代码 Agent<br/>代码生成]
M --> D[数据 Agent<br/>数据分析]
M --> R[汇总 Agent<br/>结果整合]
S -->|结果| M
C -->|结果| M
D -->|结果| M
R -->|最终输出| O[用户]
通信链路:最常见的是消息队列模式——主 Agent 把任务拆成子任务放入队列,子 Agent 取任务执行,结果写回结果队列,主 Agent 收集结果决定下一步。好处是解耦——子 Agent 之间不需要直接通信,都通过主 Agent 协调。
异常处理四层机制:
| 层级 | 策略 | 实现 |
|---|---|---|
| 超时 | 子 Agent 执行太久就 kill | 设定每个子任务的超时时间(如 30s) |
| 重试 | 返回错误时换方式重试 | 最大重试 3 次,指数退避 |
| 降级 | 子 Agent 挂了用备选方案 | 搜索 Agent 挂了用本地知识库 |
| 校验 | 子 Agent 输出不能直接信任 | 校验格式、内容、合理性 |
Skill 封装与复用
Skill 是在 MCP 之上的进一步抽象,把 Agent 的某个能力封装成"技能包"。一个 Skill 可以编排多个 MCP 工具完成一个完整的任务。
MCP 解决的是"工具怎么接",Skill 解决的是"能力怎么用"。 单个 MCP 工具就像一把螺丝刀,Skill 是"拆开一台电脑"这个能力——它知道先拧哪颗螺丝、再拔哪根线、最后怎么装回去,背后编排了多个工具的调用顺序和逻辑。
一个完整的 Skill 包含四部分:
class CodeReviewSkill:
# 1. 元信息:告诉 Agent "这个 Skill 能干什么"
name = "代码审查"
description = "审查代码质量、安全性和最佳实践"
# 2. 接口定义:输入输出的 Schema,Agent 据此决定何时调用
input_schema = {
"code": "string",
"language": "string",
"focus": "enum[security, quality, performance]"
}
output_schema = {
"issues": "list[{severity, line, message}]",
"score": "number"
}
# 3. 依赖声明:需要哪些 MCP 工具
tools_needed = ["mcp:git", "mcp:linter", "mcp:llm"]
# 4. 编排逻辑:工具的调用顺序和条件分支
def execute(self, input):
# 先用 linter 做静态分析
lint_results = self.tools["mcp:lint"].run(input["code"])
# 再用 LLM 做语义审查
ai_review = self.tools["mcp:llm"].run(
prompt=f"审查以下代码的{input['focus']}问题:{input['code']}"
)
# 合并结果
return self.merge(lint_results, ai_review)
Skill vs 硬编码 Prompt 的关键区别:
很多人会问——为什么不直接写一个 Prompt 让模型做代码审查,而要封装成 Skill?区别在于:
| 维度 | 纯 Prompt | Skill |
|---|---|---|
| 可复用性 | 每次都要复制粘贴 Prompt | 跨 Agent 共享,一处定义到处用 |
| 可组合性 | 难以拆分和重组 | 多个 Skill 可以链式编排 |
| 可测试性 | 只能靠人工验证 | 可以写单元测试验证编排逻辑 |
| 版本管理 | 散落在各处 | 独立版本,向后兼容 |
| 错误处理 | 模型自己想办法 | 显式的重试、降级、超时逻辑 |
Skill 的组合模式——三种编排方式:
graph TD
subgraph 串行
S1[Skill A] --> S2[Skill B] --> S3[Skill C]
end
subgraph 并行
P1[Skill A] --> PM[合并]
P2[Skill B] --> PM
P3[Skill C] --> PM
end
subgraph 条件分支
D1[Skill 判断] -->|条件1| DA[Skill A]
D1 -->|条件2| DB[Skill B]
end
# 串行:前一个的输出是后一个的输入
pipeline = SkillChain([
DataCleanSkill(), # 清洗数据
AnalysisSkill(), # 分析数据
ReportSkill() # 生成报告
])
# 并行:多个 Skill 同时执行,结果合并
parallel = SkillParallel([
SecurityScanSkill(),
PerformanceScanSkill(),
StyleCheckSkill()
])
results = parallel.run(code) # 三个扫描同时跑
# 条件分支:根据中间结果决定走哪条路
router = SkillRouter(
condition=lambda input: input["task_type"],
branches={
"code_review": CodeReviewSkill(),
"doc_write": DocWriteSkill(),
"data_analysis": DataAnalysisSkill()
}
)
Skill 的生命周期:
一个 Skill 从定义到执行经历四个阶段:
定义(声明元信息 + 接口 + 依赖)
↓
注册(发布到 Skill Registry,Agent 可以发现它)
↓
路由(Agent 根据用户意图 + Skill description 匹配最合适的 Skill)
↓
执行(加载依赖的 MCP 工具,按编排逻辑运行,返回结构化结果)
路由是关键环节。Agent 不是靠关键词匹配来选 Skill,而是靠语义理解——把用户意图和 Skill 的 description 做语义匹配。所以 Skill 的 description 写得好不好,直接决定了 Agent 能不能正确调用它:
# ❌ 差的 description:太模糊,Agent 不知道什么时候该用
description = "处理数据"
# ✅ 好的 description:明确输入输出和适用场景
description = """将非结构化的用户反馈文本清洗为结构化数据。
适用场景:用户评论、客服工单、社交媒体反馈。
输入:原始文本列表。
输出:清洗后的 JSON,包含 sentiment、category、key_phrases 字段。"""
现实中的 Skill 系统——以 Claude Code 为例:
Claude Code 的 Skill 系统就是这套理念的工程实现:
| Skill | 触发条件 | 编排的工具 |
|---|---|---|
init |
用户说"初始化项目" | Glob → Read → Write(生成 CLAUDE.md) |
review |
用户说"审查 PR" | Bash(gh pr view) → Read → 分析 → 输出 |
simplify |
用户说"简化代码" | Read → 分析 → Edit(重构) |
security-review |
用户说"安全审查" | Grep → Read → 分析 → 报告 |
每个 Skill 背后都是多个工具的编排,有明确的触发条件和输出格式。用户不需要告诉 Claude Code "先读文件再搜索再分析",只需要说 /review,Skill 自动完成整个流程。
| MCP | Skill | |
|---|---|---|
| 层级 | 工具层 | 能力层 |
| 解决的问题 | 工具对接标准化 | 能力复用和组合 |
| 粒度 | 单个工具 | 多个工具的编排 |
| 类比 | USB-C 接口 | 一个完整的外设 |
| 是否包含逻辑 | 否,纯接口 | 是,包含编排逻辑 |
| 是否可独立测试 | 不需要,接口即契约 | 可以,验证编排正确性 |
实际案例:Claude Code 的设计模式
Claude Code 本身就是一个 Agent 系统的典型案例:
用户输入:"帮我修一下这个 bug"
↓
Claude Code 规划:读代码 → 定位问题 → 编辑文件 → 运行测试
↓
循环执行:
1. 调用 Read 工具读取文件 → 观察代码
2. 调用 Grep 工具搜索相关函数 → 定位问题
3. 调用 Edit 工具修改代码 → 执行行动
4. 调用 Bash 工具运行测试 → 验证结果
5. 测试通过 → 任务完成;测试失败 → 回到步骤 1
三个值得参考的设计模式:
CLAUDE.md — 项目级持久化上下文
放在项目根目录的指令文件,Agent 每次启动时自动加载。相当于给 Agent 一份"岗位说明书":
## 项目
这是一个 MkDocs 文档站点,使用 Material 主题。但是有只疾旋鼬在这里捣乱。
## 常用命令
- 本地预览:`mkdocs serve`
- 部署:`mkdocs deploy`
## 编码规范
- 中文文档使用简体中文
- 代码示例使用 Python
通用意义:任何 Agent 系统都应该有"项目记忆"的注入机制,而不是每次从零开始。
Hooks — 事件驱动的自动化
Hooks 是 Claude Code 中的事件钩子机制:在工具调用前后执行自定义 shell 命令。它的核心理念是——确定性的规则应该用代码强制执行,而不是完全依赖模型。
模型是概率性的,你不能 100% 保证它每次都会遵守 Prompt 里的规则。但 Hooks 是确定性的代码——只要触发条件满足,脚本一定执行。这就像前端的 ESLint 和后端的中间件:不靠开发者"记住"规范,靠工具强制执行。
Claude Code 的 Hooks 有四个触发时机:
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
PreToolUse |
工具调用之前 | 安全拦截、参数校验 |
PostToolUse |
工具调用之后 | 自动 lint、格式化、日志记录 |
Notification |
发送通知时 | 转发到 Slack、记录审计日志 |
Stop |
Claude 停止响应时 | 清理临时文件、汇总执行过程 |
配置在 .claude/settings.json(项目级)或 ~/.claude/settings.json(全局级)中:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npx prettier --write $CLAUDE_FILE_PATH",
"timeout": 10000
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python .claude/hooks/check_dangerous_commands.py"
}
]
}
]
}
}
matcher 是正则表达式,匹配工具名称。PreToolUse 返回非零退出码时会阻止工具执行——这是实现安全拦截的关键机制。
三个实际应用场景:
场景一:自动格式化。 Claude 编辑完文件后自动跑 formatter,确保代码风格一致:
"PostToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "npx prettier --write $CLAUDE_FILE_PATH && npx eslint --fix $CLAUDE_FILE_PATH"
}]
}]
场景二:危险命令拦截。 在 Bash 执行前检查命令是否包含 rm -rf、DROP TABLE 等高危操作:
# .claude/hooks/check_dangerous_commands.py
import json, sys
data = json.loads(sys.stdin.read())
command = data.get("command", "")
DANGEROUS = ["rm -rf", "DROP TABLE", "git push --force", "chmod 777"]
for pattern in DANGEROUS:
if pattern in command:
print(f"BLOCKED: dangerous command pattern '{pattern}'")
sys.exit(1) # 非零退出码 = 阻止执行
sys.exit(0)
场景三:审计日志。 记录所有工具调用,用于事后追溯和安全审计:
# .claude/hooks/audit_log.py
import json, sys, datetime
data = json.loads(sys.stdin.read())
log_entry = {
"timestamp": datetime.datetime.now().isoformat(),
"tool": data.get("tool_name"),
"input": data.get("input"),
"session_id": data.get("session_id")
}
with open(".claude/audit.jsonl", "a") as f:
f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
sys.exit(0)
Claude Code 的源码是怎么'泄露'的?
2026 年 3 月 31 日,安全研究员 Chaofan Shou(@Fried_rice)发现 Anthropic 的 npm 包 @anthropic-ai/claude-code 的 2.1.88 版本中包含了一个 cli.js.map Source Map 文件,体积高达 59.8 MB。Source Map 本是用于调试的映射文件,但这个文件包含了完整的、未混淆的 TypeScript 源码引用——更关键的是,源码 zip 包直接托管在 Anthropic 的 R2 存储桶上,任何人都可以下载。
泄露的不是几行配置,而是整个项目:约 1,900 个文件、512,000+ 行 TypeScript 代码。这份源码被迅速归档到 GitHub 仓库 instructkr/claude-code,短短数小时内获得了近 600 Stars 和 900+ Forks。
泄露了什么? 完整的项目架构和实现细节,包括:
- 40+ 个工具的完整实现(Read、Write、Edit、Bash 等每个工具的 Schema、权限模型、执行逻辑)
- 50+ 个斜杠命令(
/commit、/review、/compact、/hooks等) - QueryEngine.ts——约 46,000 行的核心对话引擎代码,实现了完整的 Agent Loop(流式响应、工具调用循环、Thinking 模式、重试逻辑)
- 权限系统的多层模型(default、plan、auto、bypassPermissions 四种模式)
- Bridge 系统——CLI 与 IDE 扩展之间的双向通信层,包括 JWT 认证和会话管理
- 多 Agent 协调机制(AgentTool、SendMessageTool、TeamCreateTool、Swarm 模式)
- 144 个 React + Ink 终端 UI 组件和 80+ 个自定义 Hooks
- 技术栈全貌:Bun 运行时、Zod v4 校验、ripgrep 搜索、MCP SDK + LSP 协议、OpenTelemetry 遥测、GrowthBook 特性标志
- 未公开功能:Buddy 伴侣精灵、autoDream 自动梦境、KAIROS 特性标志、bughunter 自动化 Bug 搜索等
为什么会泄露? npm 发布流程中没有正确排除 Source Map 文件。cli.js.map 这种调试产物不应该出现在发布包中,但构建流水线的 .npmignore 或 files 字段配置遗漏了它。这不是安全漏洞,而是构建配置的疏忽。
工程启示: 两条教训。第一,客户端代码分发了就藏不住——即使没有 Source Map,混淆后的 JS 也可以格式化后阅读;二进制文件也可以反编译。所以不要把任何"必须保密"的逻辑放在客户端,安全决策的核心判断应该在服务端完成。第二,构建流水线的发布检查必须覆盖产物审计——哪些文件会被打进包、包体积是否异常、是否包含 Source Map 和 .env 等敏感文件。一个 npm publish --dry-run 就能在发布前发现问题。
详细的技术分析可参考:Claude Code 泄露源码架构深度分析
Permission Mode — 权限分级控制
不同操作有不同的信任级别:auto-approve(自动批准)、prompt(逐次确认)、deny(拒绝)。通用意义:Agent 的自主程度应该可以调节。
Agent 的可靠性工程
前置概念
- 上下文漂移:Agent 在多轮执行中逐渐偏离最初的任务目标
- 工具调用幻觉:模型编造不存在的工具或参数
- Prompt 注入:恶意用户通过输入操纵模型行为
上下文漂移与目标锚定
随着执行轮次增加,早期的任务描述在上下文中的注意力权重被后续内容稀释——这就是上下文漂移。三种应对方案:
| 方案 | 原理 | 实现 |
|---|---|---|
| 目标注入 | 每轮 System Prompt 重复原始任务 | "你的任务是:{原始任务},当前进度:{已完成步骤}" |
| 阶段总结 | 每 3-5 步生成进度摘要 | 用摘要替代原始完整上下文 |
| 外部监督 | 独立 Agent 监控执行轨迹 | 每 5-10 步检查是否偏离目标 |
工具调用幻觉防护
模型可能编造不存在的工具或参数。防止方法:
| 方法 | 原理 |
|---|---|
| 工具白名单 | 执行层只允许调用已注册的工具,未注册的直接拦截 |
| Schema 严格定义 | Function 的名称、参数类型、枚举值都写清楚 |
| 参数校验 | 执行前校验参数类型和取值范围 |
| 错误反馈 | 工具调用失败时,把错误信息返回给模型重新生成 |
长任务目标偏离防控
五种方法组合使用:
| 方法 | 频率 | 作用 |
|---|---|---|
| 目标注入 | 每轮 | 在 System Prompt 中重复原始任务 |
| 阶段总结 | 每 3-5 步 | 生成进度摘要,替代原始上下文 |
| 外部监督 | 每 5-10 步 | 独立 Agent 检查执行轨迹 |
| 任务分解 | 开始时 | 长任务拆成多个短任务,各有明确 I/O |
| 上下文压缩 | 持续 | 用摘要替代原始对话,释放窗口空间 |
安全防护
Prompt 注入防护
关键词匹配只是最基础的一层,实际上攻击者可以用多种方式绕过(Unicode 变体、多语言混合、编码绕过等)。生产环境需要多层防护:
| 层级 | 方法 | 说明 |
|---|---|---|
| 输入过滤 | 关键词 + 分类器 | 用专门训练的分类器检测注入意图,比关键词匹配准确率高得多 |
| 指令隔离 | XML 标签 / 分隔符 | 用明确的标记把用户输入和系统指令隔开 |
| 权限最小化 | 工具白名单 + 权限控制 | 即使被注入,模型能调用的工具和能访问的数据有限 |
| 输出校验 | 结果审查 | 检查输出是否包含系统提示内容、是否执行了未授权操作 |
| 人工确认 | 高风险操作拦截 | 删除数据、发送消息等操作必须人工确认 |
def multi_layer_injection_defense(user_input, system_prompt, output):
# 第一层:关键词过滤(快速拦截已知模式)
if detect_prompt_injection(user_input):
return {"blocked": True, "reason": "keyword_match"}
# 第二层:分类器检测(拦截未知模式)
if injection_classifier.predict(user_input) > 0.8:
return {"blocked": True, "reason": "classifier"}
# 第三层:输出校验(即使前两层漏过,也能在输出端拦截)
if system_prompt in output or contains_sensitive_pattern(output):
return {"blocked": True, "reason": "output_leak"}
return {"blocked": False}
Key 泄露防护
# ❌ 错误:Key 传给模型
prompt = f"用这个 API Key {api_key} 查询天气"
# ✅ 正确:模型只返回调用意图,Key 在执行层注入
action = llm.decide_action(prompt)
result = tools.execute(action["tool"], action["params"], api_key=os.environ["WEATHER_KEY"])
审计日志:所有工具调用都要记录(调用时间、调用者、工具名、参数、返回结果)。出了问题能追溯,也能发现异常调用模式——如果发现某次调用后工具调用模式异常(突然访问了不该访问的资源),很可能就是注入成功了。
模型训练与对齐
前置概念
- 预训练(Pre-training):在海量无标注文本上训练,学习语言的统计规律
- 微调(Fine-tuning):在特定任务的标注数据上继续训练,让模型学会特定能力
- 对齐(Alignment):让模型的输出符合人类偏好(有用、安全、诚实)
- Reward Model:学习人类偏好的模型,为 RLHF 提供奖励信号
微调 vs RAG
一句话:让模型"知道新知识"用 RAG,让模型"学会新能力"用微调。
| 维度 | RAG | 微调 |
|---|---|---|
| 改变什么 | 不改模型,改输入 | 改变模型本身的参数 |
| 知识更新 | 改文档即可,实时生效 | 需要重新训练 |
| 成本 | 低(只需向量数据库) | 高(需要 GPU 和标注数据) |
| 适用场景 | 知识库问答、文档检索 | 特定风格输出、领域推理 |
什么场景必须微调?RAG 只能提供参考资料,不能保证模型按你想要的方式去"思考"。当你需要模型学会某种推理模式或输出风格,而且要在各种输入下都稳定,就必须微调。例如:法律文书的写作风格、医学影像的判读逻辑、代码审查的判断标准。
SFT → RLHF → DPO 演进
graph LR
A[预训练模型] --> B[SFT]
B --> C[RLHF]
B --> D[DPO]
C -->|工程复杂<br/>4 个模型| E[对齐后的模型]
D -->|工程简单<br/>2 个模型| E
SFT(Supervised Fine-Tuning)— 快速迭代首选
准备好问答对,直接训。几个小时到一天就能出结果,适合快速验证和迭代。
RLHF(Reinforcement Learning from Human Feedback)— 对齐人类偏好
先训 Reward Model 再做 PPO,链路长、工程复杂、稳定性差。但 RLHF 的优势是能学到"什么是好的",而不只是"数据里有什么"。
DPO(Direct Preference Optimization)— 当前最主流的对齐方法
DPO 跳过 Reward Model,直接用偏好数据优化策略模型。数学上证明了 DPO 的损失函数等价于 RLHF,但实现简单得多:
DPO 的偏好数据格式:
损失函数:
其中 \(y_w\) 是偏好回答,\(y_l\) 是非偏好回答,\(\pi_{ref}\) 是 SFT 后的参考模型,\(\beta\) 控制偏离参考模型的程度。
DPO vs RLHF 选型:
| 维度 | RLHF (PPO) | DPO |
|---|---|---|
| 工程复杂度 | 高(4 个模型协同) | 低(2 个模型) |
| 训练稳定性 | 差(PPO 容易崩) | 好(和 SFT 一样稳定) |
| 数据需求 | 需要 Reward Model 训练数据 | 只需要偏好对 |
| 效果上限 | 理论上更高(在线探索) | 足够好,大部分场景够用 |
| 适用场景 | 大规模对齐、复杂偏好 | 快速迭代、中小规模对齐 |
DeepSeek-R1 的启示
基模能力越来越强时的破局点:
| SFT 的破局点 | RLHF/DPO 的破局点 | |
|---|---|---|
| 核心 | 数据质量而非数量 | 从人工标注走向自动反馈 |
| 策略 | 几百条高质量数据 > 几万条普通数据 | 用 Verifiable Reward 替代人工打分 |
| 例子 | 构造"模型自己想不出来的优质回答" | 代码能不能跑通、数学题对不对,自动验证 |
DeepSeek-R1 的关键发现:纯 RL(不用 SFT)可以涌现出推理能力。这说明在基模足够强的情况下,RL 的上限可能比 SFT 更高——SFT 只能学到数据里的模式,RL 能探索出数据里没有的模式。
推理优化与成本控制
前置概念
- KV Cache:缓存已计算的 Key-Value 矩阵,避免重复计算
- 量化(Quantization):降低模型参数的数值精度(如 FP16 → INT8),减少内存和计算量
- 批处理(Batching):把多个请求合并成一个 batch 一起推理,提高 GPU 利用率
- TTFT(Time to First Token):首 token 延迟,衡量模型响应速度的关键指标
KV Cache
自回归模型每次生成新 token 都要重新算前面所有 token 的注意力。KV Cache 把前面的 Key-Value 缓存起来,避免重复计算:
这是最基础也是效果最明显的优化,长序列场景下可以提速 10 倍以上。
量化
把模型参数从 FP16 压到 INT8 甚至 INT4:
| 精度 | 模型大小 | 推理速度 | 精度损失 |
|---|---|---|---|
| FP16 | 100% | 基准 | 无 |
| INT8 | 50% | ~1.5x | <1% |
| INT4 | 25% | ~2x | 1-3% |
模型路由
不是所有任务都需要最大模型。以 Anthropic 的模型族为例:
Claude Haiku 4.5 → 简单分类、格式转换、信息提取(快、便宜,$0.80/M input tokens)
Claude Sonnet 4.6 → 代码生成、文档写作、中等推理(平衡,$3/M input tokens)
Claude Opus 4.7 → 复杂推理、架构设计、核心决策(准、贵,$15/M input tokens)
graph TD
A[任务输入] --> B{复杂度判断}
B -->|简单:分类/格式化/翻译| C[Haiku<br/>快、便宜]
B -->|中等:代码/文档/问答| D[Sonnet<br/>平衡]
B -->|复杂:架构/推理/决策| E[Opus<br/>准、贵]
C --> F[输出]
D --> F
E --> F
路由的判断本身也可以用小模型来做——先用 Haiku 判断任务复杂度,再路由到对应模型,避免路由本身成为瓶颈。
批处理与限流
批处理:把多个请求合并成一个 batch 一起推理,GPU 利用率上去了,单请求的平均成本就下来了。
限流器:多 Agent 并发调用 API 很容易打爆速率限制。令牌桶算法实现:
class RateLimiter:
def __init__(self, rate, burst):
self.rate = rate
self.burst = burst
self.tokens = burst
self.last_refill = time.time()
def acquire(self):
self._refill()
if self.tokens >= 1:
self.tokens -= 1
return True
return False
def _refill(self):
now = time.time()
elapsed = now - self.last_refill
self.tokens = min(self.burst, self.tokens + elapsed * self.rate)
self.last_refill = now
关键路径的请求要优先通过,非关键的可以排队等。
多 Agent 系统的效率权衡
核心原则:简单任务用小模型,核心决策用大模型。
多 Agent 系统中的模型分配:
├── 主 Agent(规划/决策)→ Opus 级别
├── 搜索 Agent(信息检索)→ Haiku 级别
├── 代码 Agent(代码生成)→ Sonnet 级别
├── 数据 Agent(数据分析)→ Sonnet 级别
└── 汇总 Agent(结果整合)→ Haiku 级别
上下文与记忆管理
前置概念
- 工作记忆:当前正在处理的信息,容量有限(上下文窗口)
- 短期记忆:最近几轮对话,通过滑动窗口或摘要维持
- 长期记忆:历史对话、项目知识、用户画像,存储在外部(向量数据库),需要时检索
上下文预算的量化分配
实际工程中,需要把有限的上下文窗口当作"预算"来分配:
graph TD
W[200K 上下文窗口] --> A[System Prompt + 工具定义<br/>≈ 5K tokens<br/>固定开销]
W --> B[RAG 检索结果<br/>≈ 15K tokens<br/>按需加载]
W --> C[对话历史<br/>≈ 30K tokens<br/>滑动窗口]
W --> D[模型输出预留<br/>≈ 4K tokens<br/>固定预留]
W --> E[可用空间<br/>≈ 146K tokens<br/>剩余容量]
关键原则:
- System Prompt 和工具定义放前面(利用 Prompt Caching 降低重复计算成本)
- RAG 结果按相关性排序后截断,不要无限制塞入
- 对话历史用滑动窗口或摘要压缩,不要把所有历史都保留
- 预留足够的输出空间,否则模型会被截断
短期记忆压缩
三种策略解决不同场景的问题:
graph LR
A[对话历史] --> B{压缩策略}
B --> C[滑动窗口<br/>只保留最近 N 轮]
B --> D[对话摘要<br/>用模型压缩早期对话]
B --> E[关键信息提取<br/>单独存储重要约束]
C -->|适用| C1[对话历史不太重要的场景]
D -->|适用| D1[需要保留关键决策的场景]
E -->|适用| E1[需要严格执行规则的场景]
滑动窗口:最简单但也最粗暴。
对话摘要:用模型把早期对话总结成一段话。
def summarize_and_keep(history, recent_n=5):
early = history[:-recent_n]
recent = history[-recent_n:]
summary = llm.summarize(early)
return [{"role": "system", "content": f"之前的对话摘要:{summary}"}] + recent
关键信息提取:把关键决策、约束条件、用户偏好单独存一份,每轮都注入。
长期记忆
长期记忆存储在外部,需要时再检索。分层记忆结构:
graph TD
A[记忆分层] --> B[工作记忆<br/>System Prompt<br/>当前任务的关键约束]
A --> C[短期记忆<br/>上下文窗口<br/>最近几轮对话]
A --> D[长期记忆<br/>向量数据库<br/>历史对话、项目知识、用户画像]
B -->|持续注入| E[模型]
C -->|滑动窗口| E
D -->|按需检索| E
记忆衰减:不是所有历史信息都同等重要。越早的信息权重越低,定期清理过期的记忆:
def decay_and_clean(memory, decay_rate=0.95):
for item in memory:
item.weight *= decay_rate
memory = [m for m in memory if m.weight > 0.1]
return memory
工程落地总览
前置概念
- 状态持久化:将 Agent 执行状态写入外部存储,崩溃后可恢复
- Checkpoint:关键步骤的快照,出问题可回滚
- LLM-as-Judge:用强模型评估弱模型输出的自动评估方法
执行链路设计
graph TD
A[用户输入] --> B[主 Agent 规划<br/>拆分子任务]
B --> C[任务队列]
C --> D[子 Agent 并行执行]
D --> E{结果校验}
E -->|通过| F[结果汇总]
E -->|失败| G[重试/降级]
G --> D
F --> H{目标达成?}
H -->|否| B
H -->|是| I[输出最终结果]
D -.-> J[状态持久化<br/>每步写入数据库]
D -.-> K[Checkpoint<br/>关键步骤存快照]
保证连续任务正确性的五个关键点:
第一,状态持久化 — 每一步执行的结果都要写入数据库,不能只放在内存里。Agent 崩溃重启后,能从上一步接着来。
第二,Checkpoint 机制 — 关键步骤执行完就存检查点,出问题可以回滚到最近的有效状态。
第三,结果校验 — 每一步的输出不能直接信任,需要校验格式、内容、合理性。
第四,超时和重试 — @retry(max_attempts=3, backoff=exponential) + @timeout(30)
第五,人工介入点 — 高风险操作(删除数据、发送邮件、修改配置)需要人工确认。
传统 Web vs AI Agent
| 维度 | 传统 Web | AI Agent |
|---|---|---|
| 确定性 | 确定性(if-else) | 概率性(模型生成) |
| 错误类型 | 明确(404、500) | 模糊(幻觉、规划出错) |
| 调试方式 | 断点调试 | 日志分析 |
| 执行模式 | 请求-响应 | 循环执行 |
| 成本结构 | 服务器 + 带宽 | Token(按推理量计费) |
| 测试方式 | 单元测试覆盖分支 | 评估指标 + Bad Case |
| 用户期望 | 快、稳 | 智能、灵活 |
核心区别:传统 Web 是确定性的,Agent 是概率性的。这意味着不能"断点调试",只能看日志分析模型为什么做了某个决策;不能写确定性测试,更多是评估指标和 Bad Case 分析。
生成模型评估
文本生成没有像图像那样统一的自动化指标,需要多维度组合评估:
| 维度 | 指标 | 说明 |
|---|---|---|
| 准确性 | Exact Match / F1 | 有标准答案的任务(问答、分类) |
| 相关性 | RAGAS / LLM-as-Judge | 检索增强生成的答案是否忠于上下文 |
| 流畅度 | Perplexity / 人工评分 | 语言质量 |
| 有用性 | LLM-as-Judge | 用强模型打分 |
| 安全性 | Red-teaming / 分类器 | 是否生成有害内容 |
LLM-as-Judge 是实践中最常用的自动评估方法——用一个强模型(如 Claude Opus)评估弱模型的输出。需要设计好评分标准(rubric),否则打分不一致:
请根据以下标准对回答打分(1-5 分):
- 5 分:完全正确、完整、有条理
- 3 分:基本正确但有遗漏或表述不清
- 1 分:错误或完全不相关
问题:{question}
参考答案:{reference}
待评估回答:{answer}
请给出分数和理由。
实践方法:
- A/B 测试:不同模型/参数生成一批结果,人工评估哪个更好。适合模型选型阶段
- 自动化 Pipeline:用 LLM-as-Judge 批量评估,适合回归测试和持续监控
- Bad Case 分析:专门收集失败案例,分析失败模式。这是改进模型效果最高效的手段——与其看宏观指标,不如看具体哪里错了
总结
graph LR
A[用户意图] --> B[Prompt 工程]
B --> C[模型推理]
C --> D[输出约束]
D --> E{需要外部知识?}
E -->|是| F[RAG / Function Calling]
E -->|否| G[直接输出]
F --> H{需要自主决策?}
H -->|是| I[Agent 循环]
H -->|否| G
I --> J[多轮执行 + 工具调用]
J --> G
| 领域 | 核心问题 | 关键技术 |
|---|---|---|
| Prompt 工程 | 最小化意图不确定性 | System Prompt / Few-shot / CoT / Prompt Caching |
| 输出约束 | 约束输出到工程可消费的格式 | Structured Output / RAG / 多轮验证 |
| RAG | 让模型基于真实数据回答 | Embedding / 向量数据库 / 混合检索 / Rerank |
| 工具调用 | 让模型操作外部世界 | Function Calling / MCP |
| Agent 架构 | 自主决策 + 循环执行 | ReAct / 多 Agent / Skill / 权限控制 |
| Agent 可靠性 | 概率性系统的稳定性 | 目标锚定 / 幻觉防护 / 注入防护 |
| 训练与对齐 | 让模型学会新能力 | SFT / DPO / RLHF |
| 推理优化 | 效率与质量的平衡 | KV Cache / 量化 / 模型路由 / 批处理 |
| 记忆管理 | 有限窗口内放入最关键的内容 | 上下文预算 / 分层记忆 / 摘要压缩 |
| 工程落地 | 概率性系统的可靠性保证 | 状态持久化 / 结果校验 / LLM-as-Judge |
2026.05.11