Skip to content

基座模型的应用:从 Prompt 到 Agent 系统


核心理念

大模型是概率生成器,不是知识数据库。应用层的核心工作是:把人类意图翻译成模型最容易理解的形式,把模型输出约束到工程系统能消费的格式。


前置知识

在讨论具体应用之前,先厘清几个贯穿全文的基础概念。

语言模型的概率生成本质

大模型在做的事情只有一件:给定已有的 token 序列 \(x_1, x_2, \ldots, x_t\),预测下一个 token \(x_{t+1}\) 的概率分布:

\[P(x_{t+1} | x_1, x_2, \ldots, x_t)\]

这不是"理解",而是"续写"。模型根据训练时见过的海量文本,学习了"什么样的文字后面最可能跟什么样的文字"。这意味着:

  • 输出是概率性的:同一个 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 获得全局上下文表示]

注意力权重的计算公式:

\[\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V\]

直觉:\(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个?

让我们一步步思考:

  1. 净错误日志增加速率 = 3 + 2 - 1 = 4 个/分钟

  2. 达到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 格式输出,包含以下字段:
- name (string): 姓名
- age (integer): 年龄
- city (string): 城市

问题:模型有时会"跑偏",输出不合法的 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)合并:

\[\text{RRF_score}(d) = \sum_{i} \frac{1}{k + \text{rank}_i(d)}\]

其中 \(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:

向量检索 top-20
相似度过滤(去掉 < 0.7 的)
余弦去重(去掉相似度 > 0.92 的重复 chunk)
Cross-Encoder 重排序
取 top-5 塞给模型

工具调用:Function Calling 与 MCP

前置概念

  • Function Calling:模型生成结构化的函数调用请求,由外部执行层实际调用
  • MCP(Model Context Protocol):Anthropic 提出的开放协议,标准化模型与工具的对接方式
  • 工具白名单:执行层只允许调用已注册的工具,防止模型编造工具

Function Calling 机制

Function Calling 让模型能"调用工具"。模型不直接回答,而是生成一个函数调用请求:

{
  "function": "get_weather",
  "arguments": {
    "city": "北京",
    "date": "2026-05-11"
  }
}

执行层收到请求后,调用真实的天气 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. 规划:需要两个步骤——查天气、发邮件
  2. 执行步骤 1:调用 get_weather(city="北京", date="tomorrow") → 返回"有雨"
  3. 判断:会下雨,需要发邮件
  4. 执行步骤 2:调用 send_email(to="team@company.com", subject="明天有雨,记得带伞")
  5. 判断:任务完成

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_iterationsmax_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 -rfDROP 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 这种调试产物不应该出现在发布包中,但构建流水线的 .npmignorefiles 字段配置遗漏了它。这不是安全漏洞,而是构建配置的疏忽。

工程启示: 两条教训。第一,客户端代码分发了就藏不住——即使没有 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,但实现简单得多:

RLHF 流程:SFT → 训 Reward Model → PPO 优化(4 个模型,复杂)
DPO 流程:  SFT → 直接用偏好对训练(2 个模型,简单)

DPO 的偏好数据格式:

{"prompt": "写一首关于春天的诗", "chosen": "好的回答A", "rejected": "较差的回答B"}

损失函数:

\[\mathcal{L}_{DPO} = -\mathbb{E}\left[\log \sigma\left(\beta \log \frac{\pi_\theta(y_w|x)}{\pi_{ref}(y_w|x)} - \beta \log \frac{\pi_\theta(y_l|x)}{\pi_{ref}(y_l|x)}\right)\right]\]

其中 \(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 缓存起来,避免重复计算:

无 KV Cache:生成第 100 个 token 时,要重新计算前 99 个 token 的 K 和 V
有 KV Cache:直接复用缓存的 K 和 V,只计算新 token 的 Q

这是最基础也是效果最明显的优化,长序列场景下可以提速 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 sliding_window(history, max_turns=10):
    return history[-max_turns:]

对话摘要:用模型把早期对话总结成一段话。

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

关键信息提取:把关键决策、约束条件、用户偏好单独存一份,每轮都注入。

key_info = {
    "用户偏好": "喜欢简洁的回答",
    "项目约束": "必须兼容 Python 3.8",
    "已做的决定": "使用 PostgreSQL 而非 MySQL"
}

长期记忆

长期记忆存储在外部,需要时再检索。分层记忆结构:

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}
请给出分数和理由。

实践方法:

  1. A/B 测试:不同模型/参数生成一批结果,人工评估哪个更好。适合模型选型阶段
  2. 自动化 Pipeline:用 LLM-as-Judge 批量评估,适合回归测试和持续监控
  3. 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