大模型底层机制与Agent开发

现在市面上的 Agent 教程太多了,要么太浅要么太碎。

之前一直关注的博主三元同学最近出了吃透 AI Agent 开发,这门课从「底层实现级」角度拆解真实产品的工程决策,帮你建立完整的 Agent 知识体系,覆盖六大方向。

这门课学习下来让我对 Agent 有了全面的认知,下面是我的学习笔记

往期学习笔记

  1. 系统认知 Agent 六大支柱

做 Agent 开发,有些大模型本身的底层机制,你不得不了解

做 Agent 开发的人经常会遇到一类"灵异事件"------明明 prompt 写得没问题,模型却反复犯同一个错;明明上下文没超,模型却"忘了"前面的信息;调了半年参数,效果还不如别人随手写的。这些问题的根,往往不在 prompt engineering 上,而在你对模型底层运作机制的理解上。

当你开始认真做 Agent 开发时,会很快发现一件让人不安的事:模型吐出的内容,有时候看起来完全正确,但执行后却出了问题------调用了一个根本不存在的工具、忘记了早期的任务目标、或者在长对话中行为悄然漂移。这些问题不是 prompt 写得不够好能解释的,它们源于大模型本身的底层机制------Token 化、自回归生成、KV Cache、Attention 计算------这些东西在悄悄支配着 Agent 的每一次决策。

如果你不了解它们,很多 Agent 的设计决策会变成"玄学调优";理解了它们,你才能真正掌握 Agent 开发的主动权。

本文将从七个核心主题出发,系统性地解析这些底层机制,以及它们在 Agent 工程实践中的具体体现。


一、Token 化与中文的代价

做 Agent 开发,你每天都在跟 token 打交道------API 按 token 计费、上下文窗口按 token 计量、prompt 要压缩到 token 预算内。但你选的「中文」这门语言,在 token 化这件事上天然吃亏。我们先把这个账算清楚。

BPE 分词器的工作原理

BPE(Byte-Pair Encoding)是目前主流 tokenizer 的核心算法,GPT、Claude 都在用。它的工作原理是:从一个字节级的词汇表出发,反复合并高频相邻 pair,最终得到一个固定大小的词表(GPT-4 约 10 万 token)。高频词片段被编码为 1 token,低频的则被拆成多个 token。

对英文而言,空格天然分隔单词,大部分常用词直接就是 1 token。但中文没有空格分隔,BPE 被迫在字节层面切分------一个常用汉字 ≈ 1-2 token,一个中文词通常是 2-3 token。

用具体数字说话(GPT-4 tokenizer):

内容 Token 数
"apple" 1 token
"苹果" 2 token
"The weather is nice today" 5 token
"今天天气不错" 9 token

这意味着:表达相同语义,中文消耗的 token 数通常是英文的 1.5-2 倍。

逐字符拆解:为什么中文天然多消耗

"用户的输入是:""user input:" 为例:

"用户的输入是:" 包含 6 个汉字 + 1 个冒号:

  • 每个汉字 ≈ 1.5 token(常用字 1 token,稍生僻 2 token)
  • 冒号在英文符号范围内,通常 1 token
  • 估算:6 × 1.5 + 1 ≈ 10 token,实际可能 6-7 token

"user input:" 包含 11 个字符:

  • BPE 对英文按空格分隔的词处理,高频词 "user" 本身 1 token,"input" 1 token,空格和冒号合并约 1 token
  • 估算:≈ 2-3 token

深层原因不是"中文内容多",而是 tokenizer 对中文的处理粒度更细------英文空格天然分词,高频词直接对应 1 token;中文没有空格边界,BPE 被迫在子字(sub-character)级别切分,同等语义需要更多 token 编码。

这就是为什么英文 system prompt 能塞下更多内容、中文 Agent 的上下文窗口压力天然更大------同样的意思,中文每次交互都在多付 token 税。

中文 token 效率低的三个工程后果

1. 工具调用描述是重灾区

Agent 的 function calling schema 通常是英文写的------"description": "Search the web for current information"。但如果翻译成中文 schema:

  • "description": "搜索互联网获取当前信息"------token 数翻倍
  • 10 个工具,每个 tool definition 都有 name + description + parameters schema------这个差异在每轮对话都会被携带进上下文

2. 思维链(Chain-of-Thought)的隐性税

Agent 做推理时中间步骤越长越好------但中文 CoT 天然多付 1.5-2 倍的 token 费。同样的推理深度,中文 Agent 更快撞上下文窗口天花板。

3. Few-shot 例子的成本不对称

很多 prompt 里会塞 2-3 个 few-shot 示例。中文示例的 token 开销远高于英文示例,而示例通常是最"占地方"的部分。

中文 Agent 的"中文税"有多重

假设你的 Agent 每天处理 1000 轮对话,每轮平均多消耗 2000 token(中文 vs 英文的差距)。

基础数据:

  • 每天额外消耗:1000 轮 × 2000 token = 2,000,000 token/天
  • 全年额外消耗:2,000,000 × 365 ≈ 7.3 亿 token/年

GPT-4 API 定价(粗略):输入 token ≈ $0.03 / 1K tokens

计算:7.3 亿 ÷ 1000 × <math xmlns="http://www.w3.org/1998/Math/MathML"> 0.03 ≈ ∗ ∗ 0.03 ≈ ** </math>0.03≈∗∗21,900/年**

数量级:每年额外 ~2 万美元。

工程策略:核心推理英文化 + 边界翻译

这个数字解释了为什么成熟 Agent 产品往往选择:

  • 系统内部用英文 schema,工具定义、CoT prompt 全英文
  • 中文只出现在用户可见的输入/输出层
  • 翻译放在边界,而不是渗透进 Agent 推理链路

这不是歧视中文,而是工程上理性的选择------同样的推理能力,少付 50% 的 token 税。

架构图示:

css 复制代码
[用户中文] → [翻译层 zh→en] → [Agent 英文推理 + 工具调用] → [翻译层 en→zh] → [用户中文]

翻译只在两个边界各发生一次。中间 Agent 的 system prompt、function schema、CoT 步骤、工具返回结果------全链路用英文。

翻译本身也消耗 token,能抵消节省吗? 翻译一次短消息(比如 50 token 中文)大约消耗 30-40 token 英文 + 翻译指令约 20 token------合计约 50-60 token。而全流程用中文,整体要多付 1.5-2 倍的 token。如果一轮对话总 token 消耗是 2000 token,全中文 ≈ 3000 token,用边界翻译 ≈ 2000 + 60 = 2060 token。差距是 940 token/轮------翻译的额外开销相比之下微不足道。

Token 估算的安全边距

中文 Agent 的 token 估算需要加安全边距。误差来源的分布大致如下:

scss 复制代码
实际 token 数 = 估算值 × (1 + ε)

ε 的分布:
  - 纯英文:ε ∈ [0.05, 0.15]
  - 纯中文:ε ∈ [0.20, 0.35]
  - 混排 + JSON + 工具调用:ε ∈ [0.25, 0.50]
场景 建议系数 原因
纯英文 1.0-1.1 估算接近真实值
混合语言(主流) 1.5-1.8 中文占比越高越靠近 1.8
含 JSON/工具调用 2.0 中英混排 + 结构化数据误差最大

工程实践中的收敛值: SAFETY_MARGIN = 1.4 是"保守但不过激"的工程共识。选 1.4 而不是更高的原因:

  • < 1.2:对混排文本不够安全,50 轮工具调用累积后极易越限
  • 1.3-1.5:覆盖 P95 的误差场景,是实测收敛区间
  • > 1.6:过于保守,导致可用上下文窗口被白白浪费

二、自回归生成与 Agent 的控制权

你已经知道模型是逐 token 预测的。但「知道流程」和「理解它对 Agent 架构意味着什么」是两回事。

自回归生成的本质

模型生成文本的过程,是一个 token 接一个 token 往后蹦的:

css 复制代码
输入序列 [t₁, t₂, ..., tₙ]
    → 模型预测 tₙ₊₁ 的概率分布
        → 采样得到 tₙ₊₁
            → 把 tₙ₊₁ 拼回输入,变成 [t₁, ..., tₙ, tₙ₊₁]
                → 预测 tₙ₊₂
                    → ...循环...

一个关键约束:一旦生成了 tₙ₊₁,它就被写死了。你没法「撤回」或「修改」已经输出的 token。 下一个 token 的预测,完全依赖前序已生成的序列。

Agent 场景下,这意味着什么?模型一旦输出 act(调用工具),后面的 action_nameparam 都是接着往前续写的------它不会「先想好整个工具调用再输出」。模型在输出 { 的时候,还不知道这个 JSON 最后长什么样。

截断:能截断,但截断后是什么?

在流式输出模式下,技术上完全可以中止------你只需要关闭 SSE/WebSocket 连接即可。

但截断后拿到的是一个残缺的 function call,比如:

json 复制代码
{
  "name": "delete_files",
  "arguments": "{\"path\": \"/important/prod

JSON 不完整,参数残缺。此时有几条路:

1. 直接丢弃,重新请求

  • 最简单。把这次生成视为无效,重新发请求,可以加上更强的约束提示。

2. 注入干预,续写请求

  • 把截断的内容加入 history,然后让模型"续写"或"修正"------但这会污染上下文,且模型行为不可预测。

3. 尝试修复残缺 JSON(不推荐)

  • 风险极高,容易产生错误参数,比不执行更危险。

为什么"续写修正"会污染上下文

这里有一个根本矛盾:你在流式阶段发现"参数不合理",意味着你需要在模型还没生成完之前就能判断意图------这本身就很难。

模型是在做"续写",不是在做"理解"。

模型在推理时,本质上是:给定前缀 → 预测下一个最可能的 token。它并不真正"理解"这段历史是错误的、是被截断的、是需要修正的。它看到的是一个上下文窗口,然后它会顺着这个上下文的惯性往下走。

具体原因:

1. 模型可能选择"续写"而不是"纠正"

你把残缺的 JSON 放进 history,期望模型重新生成一个合理的调用。但模型的训练分布里,assistant 角色输出了一段内容,user 说"重新生成"------模型很可能把这理解为:上一条 assistant 输出是合法的、已完成的,然后基于那个"合法输出"去理解语境,而不是意识到它是一段残缺物。

2. 残缺 JSON 本身是一种"越界输入"

模型在训练时,见过的 assistant 消息几乎都是完整的、格式正确的。一个截断的、语法破损的 JSON 字符串,是一种分布外(out-of-distribution)输入。可能的表现包括:

  • 尝试在语义上"补全"那段残缺 JSON(强化了原始意图)
  • 忽略 user 的纠正指令,继续沿着 assistant 的轨迹走
  • 格式混乱,输出一个半像 function call、半像自然语言的奇怪结果

3. Role boundary 的语义被破坏了

你实际上在告诉模型:"你之前做了这个决定。"但你真正想说的是:"你之前开始做一个决定,我叫停了,现在重来。"这两者对模型来说在 token 层面是无法区分的。没有任何特殊 token 表示"这是一段被中止的输出"。

4. 注意力机制会"回看"那段残缺内容

即使你在后续 user 消息里说"请重新生成",模型在生成新的 function call 时,注意力权重仍然会分配到那段残缺 JSON 上。那段残缺内容里的路径、参数名、操作类型,都会作为上下文信号影响新的生成。

一句话总结:残缺 JSON 注入 history 的危险,不在于模型"读懂了错误",而在于模型根本不知道这是错误------它只是看到了一个异常的上下文,然后用它有限的泛化能力,在分布边缘做了一次高熵的预测。

重试时上下文"干净"为什么还不够

核心原因:模型是确定性的(在给定输入下)。

如果你重试时,输入完全一样(同样的 system prompt、同样的 user 消息、同样的 temperature),那么:

复制代码
相同的前缀 → 相同的概率分布 → 大概率相同的输出
  • temperature=0 时这是严格确定的,会100%重现。
  • temperature>0 时引入了采样随机性,但只是在同一个概率分布上采样------如果原始参数在该分布下是高概率输出,重现的概率依然很高。

你清理了 history,但你没有清理"导致错误输出的原因"。

被截断的那个 function call 之所以参数不合理,根源只有两个地方:

  1. 输入本身有歧义或不足:user 的请求描述不够精确,导致模型在参数空间里选择了一个合理但危险的解。
  2. 模型的 internal bias:模型在训练数据中见过大量类似的 function call pattern,形成了某种"默认填参"倾向。这是权重层面的问题,不是上下文层面的问题------清理 history 根本触达不到它。

什么情况下重试会有效? 重试有效,当且仅当你在重试时改变了某些东西:

改变了什么 效果
提高 temperature 增加随机性,可能逃出局部高概率区域,但不可控
修改 system prompt,明确约束参数范围 改变了条件分布,有效
修改 user message,提供更精确的意图描述 改变了条件分布,有效
加入 negative example(告诉模型不要生成什么) 有效,改变了上下文语义
使用 structured output / grammar sampling 从生成机制层面约束,最有效
什么都不改,只是重试 基本无效

这里隐藏的更大问题:

复制代码
上下文的干净 ≠ 输入的充分

重试消除的是"残缺 JSON 污染上下文"的问题,但它无法消除"原始输入不足以约束模型行为"的问题。很多 Agent 框架的重试逻辑只做了前者,以为清空 history 就够了,结果陷入一个死循环:截断 → 重试 → 再截断 → 再重试。

所以一个健壮的 Agent 框架,在截断并重试之前,必须先回答一个问题:我知道上一次为什么错了吗?如果不知道,重试只是在消耗 token。

纯靠采样随机性能逃出错误吗

假设模型在生成错误参数时,概率分布长这样:

less 复制代码
P("/important/prod/...") = 0.61 ← 模型选了这个(错误)
P("/tmp/safe_path/...") = 0.28 ← 这个是正确的
P(其他) = 0.11

temperature > 0 时,采样会在整个分布上随机抽取。正确答案有 0.28 的概率被选中------什么都不改,重试一次,确实可能逃出错误。这在概率上是真实的,不是幻觉。

但代价是什么?

1. 你失去了对"为什么成功"的解释权

你无法确定这是因为采样随机性恰好选到了正确分布区域,还是某种我们没意识到的上下文差异起了作用。这次成功不可复现、不可归因、不可学习。

2. 重试次数和错误率之间是乘法关系,不是加法

假设每次重试,错误概率是 p=0.61。那么:

ini 复制代码
连续 n 次都错的概率 = 0.61^n

n=1:  61%
n=2:  37%
n=3:  23%
n=5:   8%

看起来多试几次就能收敛------但这个计算隐含了一个危险假设:每次采样是独立的。实际上,如果错误来自模型的 internal bias,每次采样并不是真正独立的,它们都在同一个偏斜的分布上采样。高概率区域会被反复命中,逃出去的概率比理论值更低。

3. 你无法区分"采样逃出"和"采样漂移"

temperature 升高时,不只是让正确答案更容易被选中,它让整个分布变平,包括那些更离谱的错误也获得了更高的采样概率。你想用随机性逃出一个错误,但你同时打开了跌入更多错误的门。

纯靠采样随机性逃出错误,本质上是在用概率覆盖替代问题修复。 它在工程上对应一种真实存在的策略------Best-of-N sampling:生成 N 个候选,用 verifier 选最好的那个。但它成立的前提是:你有一个可靠的 verifier。没有 verifier,Best-of-N 就退化成:随机试,不知道哪个对,随便选一个------这在 Agent 框架里是不可接受的,因为 function call 的执行是有副作用的,你不能先执行 N 次再选最好的那个。

结论:随机逃出是一种"侥幸",不是一种"策略"。除非你有 verifier,否则它不应该出现在 Agent 框架的设计里。


三、KV Cache 与前缀匹配

Attention 的工作原理

理解 KV Cache 之前,需要先理解 Attention(注意力机制)。

你在 Google 搜索的时候,发生了什么?

  1. 你输入一个搜索词------"KV Cache 是什么"
  2. Google 拿你的搜索词去匹配所有网页的标签和关键词
  3. 匹配度高的网页,把它的内容返回给你

Attention 做的事情完全一样:

  • Query(Q):当前正在生成的 token 会问一个问题------"我需要什么信息来决定下一个词?"
  • Key(K):前面每一个 token 都有一个标签------"我包含什么类型的信息?"
  • Value(V):前面每一个 token 的实际内容------"这是我的具体信息。"

生成新 token 的时候,模型拿当前 token 的 Query,去和前面所有 token 的 Key 做匹配。匹配度高的,就把对应的 Value 拿过来,加权混合,作为生成下一个 token 的依据。

这就是 Attention 的全部了。 就是一个"查询-匹配-取值"的过程,只不过这个过程是可以被训练的------模型通过大量数据学会了怎么生成好的 Query、Key 和 Value。

KV Cache 在做什么

每生成一个新 token,它的 K 和 V 向量会被计算并存储。下一次生成时,历史 token 的 K 和 V 直接从缓存读取,不需要重新计算。这就是 KV Cache

用考试做比喻:每道题都需要翻课本找公式,KV Cache 就像把常用公式抄在草稿纸上------后面的题直接看草稿纸就行,不用每次都翻书。

但 KV Cache 有一个关键限制:它是基于"前缀匹配"的。

举个例子。如果 Agent 第一轮对话发给模型的是 [System Prompt] + [用户消息1] + [模型回复1],第二轮是 [System Prompt] + [用户消息1] + [模型回复1] + [用户消息2]------前面完全一样,只是末尾加了新内容,所以前面所有 token 的 KV Cache 都能复用。

但如果在 System Prompt 开头加了个时间戳 "当前时间:2026-04-02 08:00:00",每次都变,第一个 token 就不一样了,后面所有的 KV Cache 全部作废,需要从头重新计算。

这不是理论上的问题。Anthropic 的 API,缓存命中和缓存未命中的价格差 10 倍。 一次工具定义变动,一次调用就贵 10 倍。

KV Cache 失效的三种场景

场景 1:缓存被驱逐

KV Cache 存储在 GPU 显存里,而显存是有限的。当 session 变长,缓存容量被耗尽时,系统必须驱逐部分历史 token 的 K、V 才能为新 token 腾出空间。被驱逐的 token 下次再被 attend 到时,需要从头重新计算它们的 K 和 V------缓存收益归零。

场景 2:前缀中段被修改(连锁失效)

KV Cache 匹配是"从头开始"的连续匹配。如果在 position N 处修改了内容,position N 的 K/V 必须重算;position N+1、N+2... 在 attend 时看到了 position N 的新内容,它们的 K/V 也必须变。

关键: 不是"前缀变了就失效",而是"前缀的某个位置被修改了才失效"。末尾追加新消息不影响已计算的 K/V。

场景 3:session 断开

KV Cache 是推理引擎在单次 continuous session 内的内存结构,session 断开即释放。

"占位后回填"反模式

Agent 框架里有一种常见的反模式:

python 复制代码
# 反模式:先占位,后回填
history.append({role: "tool", content: "pending..."})
result = execute_tool()
history[-1]["content"] = result   # 这是修改,不是追加!

占位再回填,在 history 数据结构层面看起来像"更新",但对推理引擎来说,它看到的是:position N 上的 token 内容变了------触发从 N 开始的连锁失效。

正确的做法:等 tool 执行完毕,拿到结果后再追加,绝不占位。 追加是 O(1),回填是 O(n) 全量重算。

为什么前缀越稳定,KV Cache 收益越高

KV Cache 的核心收益是"省掉重复计算"。前缀越稳定,已计算的 K/V 越能被复用。

sql 复制代码
System Prompt 放最前面 → 每轮对话共享相同的 8000 token 前缀
工具调用结果追加到末尾 → 不影响已计算的 K/V

Prefix Caching(vLLM): 每次新请求进来,先把 token 序列做 hash。如果某个前缀块的 hash 和缓存里已有的某个块完全一致,直接复用同一份 KV Cache 块。

跨请求共享的条件:

  1. 所有请求的 system prompt 在 token 层面完全相同
  2. 引擎开启了 prefix caching 功能
  3. 推理引擎不感知"Agent 实例"------共享是请求级别(hash 匹配),不是实例级别

KV Cache 与上下文窗口之间的张力

复制代码
KV Cache 的激励:前缀越长越稳定,复用收益越高
窗口限制的压力:序列越长,截断越不可避免

两者同时优化是不可能的。什么时候应该主动放弃 Cache 选择全量重算?

  1. 上下文已经被污染:历史里有错误的 tool call、被截断的残缺输出。继续复用等于在错误基础上推理,全量重算是清洁成本。

  2. 任务发生根本性状态跳转:从"探索"进入"验证"阶段后,早期 tool result 贡献接近零。全量重算换来更高质量的上下文。

  3. 需要注入新的全局约束:修改 system prompt 本身就会触发全量重算。与其假装缓存还有效,不如明确标记为全量重算点。

  4. 缓存碎片化:当缓存命中率低于 30%,每次新 token 仍要重算大部分历史 K/V,全量重算反而更干净。

什么时候应该不惜一切保住 Cache?

  1. 高频、短增量的 tool call 循环:Agent 在一个稳定的 system prompt 下反复调用工具,每次只追加少量新内容。这是 Cache 收益最大的场景。

  2. 并发的多 Agent 共享同一 system prompt:多个 Agent 实例跑同一个 system prompt,推理引擎可以跨实例共享这段前缀的 Cache。


四、上下文窗口的结构性限制

窗口满了会发生什么

模型本身不会"自动丢弃"------截断发生在推理引擎或 Agent 框架层。框架必须决定:把哪些 token 扔掉,才能腾出空间放新的 tool result。

三种丢弃策略:

1. 截掉最早的内容(滑动窗口)

实现最简单,但代价极高------system prompt 和早期任务目标最先消失。模型在后续生成时,不再知道自己的角色定义和初始约束,行为会悄悄漂移,且不会报错。

2. 压缩中间历史(摘要替换)

保住了 system prompt,但摘要引入了信息损失------细节、具体数值、tool 的原始输出都可能在压缩中消失,且摘要本身是模型生成的,可能引入幻觉。

另外,摘要替换属于中段修改------KV Cache 从摘要插入点开始全部失效,前一节的代价在这里兑现。

3. 选择性丢弃(保留关键节点)

人工或启发式标记哪些消息"不可丢"(system prompt、关键 tool result、用户明确的约束),优先丢弃冗余的中间轮次。实现复杂,但信息保留质量最高。

静默漂移:最危险的隐藏影响

最危险的不是报错,是静默漂移。 截断不抛异常,模型不知道自己"忘了"什么,它只是基于当前窗口内的内容继续生成,表现完全正常。

具体隐患:

  1. 目标遗忘:system prompt 被截掉后,角色约束消失。一个被定义为"只能读取,不能写入"的 Agent,可能在窗口截断后开始执行写操作------没有任何报错,只有行为的悄然变化。

  2. 工具状态断层:早期 tool call 的结果被丢弃后,模型可能重复查询,或基于已经过时的状态做决策。它不知道自己曾经查过,因为那段历史已经不在窗口里了。

  3. 一致性幻觉:回复语气和格式连贯,但推理依据已经不完整。这是最难被用户或监控系统发现的失效模式。

如何主动检测静默漂移

检测必须在框架层主动埋入,不能依赖模型的自我感知。

输入层------Token 计数监控:

python 复制代码
if estimated_tokens > WINDOW * 0.8:
    logger.warning("窗口超过 80%,触发警戒")
if estimated_tokens > WINDOW * 0.95:
    history = compress_history(history)
    flag_context = "limited_context_mode"

关键内容存活校验:

用 hash 检查 system prompt 不够(部分截断时 hash 完全变了)。正确做法是分段锚点校验

python 复制代码
anchors = [
    {"id": "role_def", "text": "你是一个只读的数据分析 Agent"},
    {"id": "constraint_1", "text": "禁止执行任何写入操作"},
    {"id": "constraint_2", "text": "所有输出必须引用数据来源"},
]
# 截断后扫描窗口,检查每个锚点是否还在
# 高权重锚点(权限边界/禁止操作)丢失 → fail fast
# 低权重锚点丢失 → 记录告警,可继续

执行层------重复调用检测:

同一个 tool、同一组参数被调用超过 N 次------很可能是因为早期 tool result 已被截断,模型"忘了"自己查过。

调用序列一致性校验: Agent 的 tool call 通常有隐含的依赖顺序(先读后写,先查后决策)。如果检测到调用序列违反了这个顺序,可能是因为"读取"那一步的历史已经被截断。

参数来源可追溯性: 每个 tool call 的关键参数,应该能在当前窗口里找到来源(用户输入、或者某次 tool result)。如果某个参数找不到来源,说明它的依据已经不在窗口里了------模型在凭空填参,这是幻觉的前兆。

输出层------自我一致性探针:

在高风险决策前插入探针:"当前任务的目标是什么?"让模型复述,与 system prompt 定义对比。偏差说明 system prompt 的影响已经减弱,可能已被截断或稀释。

检测到截断后:fail fast 还是降级

取决于丢失的是什么,不应该是统一策略:

  • 权限边界 / 禁止操作丢失 → fail fast:不知道自己不能做什么,继续执行风险不可控。中止,返回明确错误,等待人工干预或上下文重建。
  • 角色定义丢失 → 降级 + 注入:把 system prompt 的核心部分重新注入到当前窗口头部。代价是挤占新 token 空间,但比漂移执行安全。
  • 格式要求 / 低权重内容丢失 → 告警 + 继续:记录日志,输出层加强校验,不中止执行。

Fail fast 的具体形态:

sql 复制代码
1. 暂停当前 tool call 队列,不执行任何副作用操作
2. 向上层返回结构化的失效信号:
   {"status": "context_degraded", "missing_anchors": [...], "severity": "critical"}
3. 由上层决策:重建上下文后重试 or 通知用户 or 人工介入

关键是:fail fast 的边界是"不执行副作用",而不是"立刻崩溃"。Agent 应该在安全状态下停下来,而不是在受损状态下继续跑。

如何区分"窗口截断"和"Attention 稀释"

这是两个不同的失效机制,诊断方法完全不同:

窗口截断:内容物理上不在窗口里了,模型没有遗忘,它从未看到。

Attention 稀释:内容还在窗口里,但在 25k token 的序列中,10k 之前的 K 向量在 attention 计算里分配到的权重极低,信息没有被有效提取。

诊断方法:

第一步:确认内容是否还在窗口里

直接检查:把当前发给模型的完整 prompt token 数打出来,确认是否超过 32k。如果没超过,物理截断可以排除,问题在 Attention 稀释。

第二步:设计探针请求

构造一个问题,答案只能从疑似被遗忘的内容里找到,且答案是精确的、不可能被猜到的:

arduino 复制代码
如果模型回答正确 → 内容在窗口里,且被成功 attend 到
如果模型回答错误但方向对 → Attention 稀释,信息被部分提取
如果模型说"我没有看到相关信息" → 可能截断,或严重稀释
如果模型编造了一个看起来合理的答案 → 稀释导致的幻觉填补

第三步:改变内容位置观察行为变化

把疑似被遗忘的内容复制一份追加到 history 末尾,重新问同一个问题:

复制代码
如果模型现在能正确回答 → 原始位置的 Attention 权重不足,是稀释问题
如果模型还是回答不了 → 内容本身有问题,或截断判断有误

两种原因对应的处理方向:

css 复制代码
截断 → token 预算问题,用策略 B+C 压缩序列长度
Attention 稀释 → 重要内容被淹没,处理方向是:
               把关键信息提取出来,在每轮 Thought 之前重新注入到上下文近端
               而不是试图让模型从 25k 序列的头部自己挖出来

五、Temperature、Top-P 与确定性幻觉

Temperature = 0 不保证确定性

模型每生成一个 token,内部分三步:

  1. 给所有候选词打分(Logits):模型的词汇表里有几万个 token,每生成一个新 token,模型会给每个 token 打一个原始分数,表示"根据前面的上下文,这个 token 接下来出现的可能性有多大"。

  2. 把分数变成概率(Softmax):用 softmax 函数把原始分数转换成概率分布,所有候选词的概率加起来等于 1。

  3. 根据概率采样(Sampling):拿到概率分布后,模型从里面"抽签"选一个 token。

Temperature 控制的是 softmax 输出的概率分布形状:

temperature 效果
接近 0 概率分布非常尖锐,最高分 token 几乎 100% 被选中,输出确定、保守
= 1 正常的概率分布,有一定随机性
> 1 概率分布更平坦,低分 token 也有机会被选中,输出更有创意,但也更容易胡说八道

T → 0 时,分布退化为 argmax------概率最高的 token 被确定性地选中,其他 token 概率归零。理论上,temperature=0 = greedy decoding = 完全确定性。

但实际上,temperature=0 不保证确定性:

1. 浮点运算的非确定性:GPU 并行浮点计算结果依赖运算顺序,受 batch size、GPU 型号、驱动版本、并行规约的线程调度影响。两个 logit 值非常接近时,argmax 可能翻转。

2. 批处理的影响:推理引擎通常把多个请求打包成 batch 一起计算。batch 组成不同,数值路径可能不同。

3. 模型并行的不确定性:多 GPU 张量并行时,加法顺序不同可能导致 logit 排名翻转。

工程结论: temperature=0 给你的是高度稳定性 ,不是严格可复现性。如果 Agent 逻辑依赖输出精确一致,应该在执行层做幂等设计,而不是依赖模型层的确定性。

高 Temperature + 低 Top-P 为什么互相抵消

Temperature 的作用(分布摊平):

ini 复制代码
原始分布:  [0.60, 0.25, 0.10, 0.03, 0.02]
T=1.2 后:  [0.48, 0.27, 0.15, 0.06, 0.04]  ← 高概率压低,低概率抬高

Top-P=0.1 的作用(候选集截断):

ini 复制代码
T=1.2 后:  [0.48, 0.27, 0.15, 0.06, 0.04]
累加:       0.48 → 已经超过 0.1
保留:       [0.48] → 只剩第一个 token

冲突的本质: Temperature 想扩大候选范围,Top-P=0.1 紧接着把候选范围压缩到最小。温度在 T=1.2 时对分布的改动,被 Top-P=0.1 的筛选完全吃掉了。

而且有一个反直觉的地方: Temperature 越高,Top-P=0.1 的截断反而越容易变得更激进。原始分布越尖锐,第一个 token 概率越高,Top-P 0.1 可能还能保留 1-2 个 token。Temperature 把分布摊平后,第一个 token 概率降到 0.48,反而更容易单独就超过 0.1 的阈值,候选集更小。

正确的组合逻辑:

目标 Temperature Top-P
稳定(tool call) 0 ≈ 0.9(宽松过滤)
有创意但不飘 0.7-0.9 0.9-0.95
真正的高多样性 1.0+ 0.95+

幻觉的 logit 偏差为什么不能靠 Temperature 逆转

假设模型把 list_users 记成了 get_users,此时:

ini 复制代码
get_users:   logit = 8.5  → 概率 ≈ 0.91  ← 幻觉 token
list_users:  logit = 5.2  → 概率 ≈ 0.07  ← 正确 token

这不是"模型不确定随机选错了",而是模型非常确定地选了错误答案。logit 差值 3.3,对应概率比超过 10:1。

提高 Temperature 后:

ini 复制代码
T=1.0:  get_users 0.91,  list_users 0.07
T=1.5:  get_users 0.75,  list_users 0.16  ← 正确答案概率上升
T=2.0:  get_users 0.62,  list_users 0.24
T=3.0:  get_users 0.48,  list_users 0.33

正确答案的概率确实在上升------但分布摊平不是只抬高了 list_users,所有 token 都被抬高了:

yaml 复制代码
T=2.0 时:
  get_users:   0.62  ← 幻觉,概率下降了
  list_users:  0.24  ← 正确,概率上升了
  fetch_users: 0.08  ← 另一个幻觉,从 0.01 涨到 0.08
  load_users:  0.04  ← 又一个幻觉

你在提高正确答案被采样概率的同时,也在提高各种其他错误答案的概率。

更危险的是 Tool Call 的自回归特性------第一个 token 选错,后续所有 token 都在错误前缀上继续生成。Temperature 升高让错误前缀的种类更多,你得到的不是"正确答案出现了",而是"各种各样的错误答案都出现了"。

核心结论: Temperature 只能"摊平"偏见,无法"逆转"偏见。幻觉来自 logit 层面的强偏差时,靠采样碰运气就像大海捞针。正确修复方向:改变输入(few-shot / 文档注入)或改变权重(fine-tuning),而不是在错误的分布上多采样几次。


六、幻觉的底层成因

这是整个 Agent 开发中最让人不安的话题。

预训练先验 vs 上下文约束

模型在生成每个 token 时,logit 可以拆成两部分:

scss 复制代码
logit(token) ≈ 预训练先验信号 + 上下文调制信号

预训练先验信号: 来自权重矩阵,编码了这个 token 在训练语料里类似上下文下出现的频率。download_file 在数百 GB 代码和文档里出现过无数次,这个信号极强。

上下文调制信号: 来自当前输入的 attention,system prompt 里的 tool list 告诉模型"只有 search 和 read_page"。这个信号只出现在当前上下文里,一次。

为什么先验信号系统性更强?

1. 参数量 vs 上下文长度的不对称

先验写在"石头"(权重)上,刻了几千亿次;上下文写在"沙子"(一次性 attention)上,只写了一次。两者在 logit 上的贡献量级本来就不对等。

2. Attention 的稀释效应

上下文越长,tool list 的 attention 权重被稀释得越厉害。预训练先验不经过 attention,直接体现在 FFN 层的权重激活里,不被稀释。

3. 训练目标没有"遵守上下文约束"的直接监督

模型没有被显式训练成"上下文说只能用 X 工具时,就只生成 X 工具"。这个能力是 instruction tuning 和 RLHF 阶段试图补充的,但它叠加在预训练先验之上,而不是替换它。

为什么 download_file 这个案例特别危险

download_file 不只是出现过------它出现在大量工具调用、Agent 代码、函数定义的上下文里。模型不只学到了这个词,它学到了"在需要下载文件时,调用 download_file 是合理的行为"这个模式。

arduino 复制代码
模型内部发生的事:
  语义激活:"需要获取远程内容"
  先验模式:"download_file 是做这件事的正确工具"  ← 强信号
  上下文约束:"只有 search 和 read_page"          ← 弱信号
  结果:先验胜出,生成了 download_file

连贯的虚构为什么比乱码更难检测

乱码的特征:局部 logit 崩塌------某个位置模型真的不确定,概率分布很平,生成了低概率 token。容易检测:JSON 解析失败,或 schema 校验不通过。

连贯虚构的特征恰恰相反:每一步的 logit 都很集中,模型在每个位置都非常"自信"。

json 复制代码
生成 download_file 时:logit 集中,自信
生成 "url": 时:logit 集中,自信(训练语料里总是这么跟的)
生成 url 值时:logit 集中,自信(见过大量 URL 格式)

整条链路上,没有任何一个位置的 logit 分布是平的。从 logit 分布的形状上,真实的 search 调用和虚构的 download_file 调用几乎无法区分

这就是为什么"自信度"不能作为幻觉检测的信号------模型对自己的错误和正确一样自信。

虚构调用的 logit 来源:

sql 复制代码
真实的 search 调用:
  上下文约束信号:"tool list 里有 search"    ← 有贡献
  预训练先验信号:"search 是常见工具"         ← 有贡献
  两个信号方向一致,相互加强

虚构的 download_file 调用:
  上下文约束信号:tool list 里没有它          ← 信号为负,但很弱
  预训练先验信号:"download_file 是常见工具"  ← 强正信号
  两个信号方向相反,但先验强度压过约束

幻觉的自举放大机制

一旦 download_file 被生成并成为前缀,后续所有 token 都在"已经调用了 download_file"这个条件下续写。训练语料里 download_file 后面跟 url、file_path 的模式被激活,模型沿着这条高概率路径一路走下去:

复制代码
虚构的起点:一个错误 token(download_file)
自回归的放大:每一步都在强化这个虚构的内部一致性
最终产物:一个结构完整、参数合理、格式正确的虚构工具调用

内部一致性越高,越难从输出形态上发现问题------因为所有的检测规则(JSON 合法、参数类型正确、格式符合预期)都会通过。

白名单拦截反而强化了幻觉

每次循环的结构:

复制代码
模型生成 download_article → 白名单拦截 → 注入错误反馈 → 模型再次生成

每次被拦截的虚构调用留在 history 里,反而在强化模型生成同类幻觉的倾向:

bash 复制代码
第一次拦截后 history 里有:
  {"action": "download_article", "url": "..."}  ← 完整结构,高质量的错误示范

第二次生成时,模型 attend 到这条记录:
  预训练先验:download_article 是合理工具       ← 强信号
  history 里的完整调用:进一步强化了这个模式    ← 额外助推
  纠正信号:"请用 search 或 read_page"          ← 弱信号

循环次数越多,这个自我强化效应越强。

结构性代价:

  1. 每次被拦截的虚构调用强化了生成幻觉的先验
  2. 窗口被低质量内容填满,挤占有效上下文
  3. 纠正信号边际效用递减
  4. KV Cache 把所有这些累积效应永久保存

正确处理方式: 拦截发生时,不把虚构工具调用原样留在 history 里,替换为中性标记 "[无效调用已移除]",或直接从 history 中删除(接受从该位置开始的 KV Cache 失效)。同时检查 tool list 在当前窗口的 attention 权重是否已被稀释,必要时重新注入。

Attention 的数学结构为什么无法区分"事实正确"和"统计高频"

这是整个幻觉问题的根源,直接从 Attention 的数学结构推。

Attention 在计算什么?

scss 复制代码
Attention(Q, K, V) = softmax(Q · Kᵀ / √d) · V

这个计算做的全部事情是:在向量空间里度量相似性,然后加权聚合。

Q·Kᵀ 衡量的是"当前位置的查询方向"和"历史位置的索引方向"在几何上的对齐程度。这个对齐程度完全是训练语料共现统计的产物。

arduino 复制代码
"巴黎是法国的首都" → 高频共现 → 向量对齐强 → logit 高
"download_file 后跟 url 参数" → 高频共现 → 向量对齐强 → logit 高
"某个生僻但正确的历史事件" → 低频 → 向量对齐弱 → logit 低

"事实"在哪里都不在。 Attention 的数学结构里只有:这两个向量在几何上有多近。而向量的位置由训练语料的共现统计塑造,和"真值"无关。

真值校验需要的是一个参照系------某个外部的、独立于训练语料的标准。 但 Transformer 的前向传播是一个纯粹的矩阵运算链,没有任何节点可以挂载这样的参照系。

这不是 Attention 机制的缺陷,而是它的设计目标本来就不是真值校验。 它被设计来做:给定前缀,预测统计上最可能的续写。它做得非常好。但"统计上最可能"和"事实上正确"是两个不同的目标函数。


七、延迟、成本与架构取舍

Prefill 和 Decode 的成本结构

Prefill(新输入处理):

一次性计算,把新增 token 变成 KV 向量:

scss 复制代码
新 token 数 = n_new
历史 token 数 = n_hist

Q/K/V 计算:O(n_new × d)
Attention:O(n_new × n_hist)

所有新 token 并行计算,GPU 利用率高。这是并行计算------硬件利用率高,但序列越长计算量越大。

Decode(逐 token 生成):

自回归串行,每个新 token 都要做一次完整前向:

scss 复制代码
生成 M 个 token,每个 token 都要:
  Q_new 计算:O(d)
  Attention(Q_new, 所有历史 K, 所有历史 V):O(n_hist)
  K_new, V_new 计算并缓存:O(d)

总 decode 计算量:O(M × n_hist)

每次新 token 生成,都要从显存读取全部历史 K、V(带宽),再做矩阵乘法(算力)。

KV Cache 分别做了什么、没做什么:

Prefill Decode
省了什么 历史 K、V 不重算 历史 K、V 不重算
没省什么 新 token attend 历史 K 的计算量 每生成一个 token,都必须 attend 到全部历史 K

真正被省掉的: K、V 的重复计算。

省不掉的: Attention 的读取带宽。每次新 token 生成,都要从 HBM(高带宽显存)读取所有缓存的 K、V。

为什么"Token 预算是乘法因子"

从计算量公式出发:

ini 复制代码
总计算量 ∝ O(n × m)

n = 历史序列总长度
m = 输出 token 数

KV Cache:将前缀重算省掉,节省约 40%(系数 0.6)
Batch 优化:提高 GPU 利用率,吞吐提升 50%(系数 0.5)

叠加效果:O(n × m) × 0.6 × 0.5 = O(n × m) × 0.3

KV Cache 和 Batch 都是百分比折扣,Token 预算决定的是 n 本身。

scss 复制代码
把 history 从 20k 压到 5k:
原始:    O(20000 × m) × 0.6 × 0.5
压缩后:  O(5000 × m) × 0.6 × 0.5

n 从 20000 变成 5000,所有后续的系数优化都在这个更小的底数上生效。

折扣可以叠加,但底数变化的效果是乘法的: O(5000 × m) × 0.3 是 O(20000 × m) × 0.3 的 1/4,和折扣无关。

一句话:调整 token 预算就是调整 O(n) 里的 n------后者是乘法因子,前者是百分比折扣。

三种延迟优化策略的作用点与幻觉风险

策略 A:摘要压缩前 20k → 2k

主要作用于:Prefill (减少前缀总长度)+ Cache 重算(中间修改触发连锁失效)。

把 20k 压成 2k,后续每轮的输入序列缩短了 18k token,prefill 成本大幅下降。但压缩 = 中段替换,从替换位置到末尾的所有 K、V 全部失效,需要一次性重算。

策略 B:system prompt 和工具定义移到缓存前缀

主要作用于:Cache 失效防御(降低失效的波及范围)。

这不是压缩,是结构调整。把最稳定、最不会被修改的内容固定在前缀最头部,保护它不被后续操作触发失效。对 prefill 的直接影响有限------总 token 数没变,只是分布变了。

策略 C:tool result 只保留关键字段

主要作用于:Prefill(每轮新增的 token 数减少)。

每轮追加的 tool result 是 prefill 成本增长的主要来源。完整 JSON 可能几百到几千 token,裁剪到关键字段可能只剩几十 token。这是在调整 O(n) 里的 n------直接减少每轮新增的序列长度,效果在每一轮都会体现,没有一次性的突发重算代价。

哪种策略最可能引入幻觉风险?

策略 A 风险最高。

摘要是有损压缩,且损失不均匀。被压掉的 18k token 里,可能包含具体的工具调用参数、中间结果的原始数值、边界条件的处理记录。摘要会保留"发生了什么",但会丢失"具体怎么发生的"。后续轮次如果需要依赖这些细节做推理,模型会在细节缺失的条件下填补------这就是幻觉的入口。

更关键的是:摘要本身是模型生成的,它会把摘要阶段的先验偏差固化进 history。

策略 B 和 C 的幻觉风险相对低:

策略 B 不改变任何内容,只改变结构位置,信息完整性没有损失。

策略 C 裁剪 tool result,有一定信息损失,但损失是可控的、显式的------你知道裁掉了哪些字段,可以在裁剪逻辑里保留语义关键字段,丢弃冗余结构。

推荐组合是 B + C,A 作为最后手段。


总结

Agent 开发中,这些底层机制共同支配着每一次决策:

Token 化决定模型接收什么------中文天然在"交 token 税",这个差距会在长对话中被放大。年万美金级的额外成本,主要来源就是中文的编码效率差异。工程策略是"核心推理英文化 + 边界翻译",而不是"全链路英文"。

自回归生成决定每个新 token 如何基于已有内容生成------模型是"边说边想"的,流式响应是天然的,但 JSON 要攒完才能解析。截断是一种应急手段,真正的防护应该在生成约束层或执行层。重试只在你改变了输入条件时才有效,"干净上下文"和"充分输入"是两件不同的事。

KV Cache 决定已接收内容的计算效率------前缀越稳定复用收益越高,但前缀一旦变化就触发连锁失效。占位后回填是常见的反面模式。KV Cache 和上下文窗口之间存在结构性张力,两者同时优化不可能,需要根据场景取舍。

上下文窗口限制 迫使你必须主动管理上下文------截断不抛异常,静默漂移是最危险的敌人。分段锚点校验是检测静默漂移的正确方法。KV Cache 的静默劣化会同时放大所有下游的脆弱性,这是 LLM 系统区别于传统软件工程的最本质特征------错误不在发生点爆炸,而是沿着因果链悄悄流到最意想不到的地方。

Temperature 和采样影响输出的确定性------temperature=0 不保证严格确定性,高 Temperature + 低 Top-P 会互相抵消。幻觉来自 logit 层面的强偏差时,靠提高 temperature 采样是在错误的分布上碰运气,正确修复方向是改变输入或改变权重。

幻觉的根因是预训练先验和上下文约束的信号强度不对称------先验写在权重里不被稀释,约束是一次性的 attention。模型无法区分"事实正确"和"统计高频",因为 Attention 的数学结构只计算向量空间的几何对齐,而向量位置由训练语料的共现统计塑造。白名单拦截是必要但不够的------拦截后必须同时清理 history,否则每次被拦截的虚构调用都在强化下一次幻觉的先验。

延迟和成本控制的核心是 token 预算------B + C 组合是首选策略,B 保护前缀稳定性,C 控制每轮增量,A 是最后手段。Token 预算是乘法因子,KV Cache 和 Batch 优化是百分比折扣,调整 n 的效果远大于调整系数。

真正危险的,不是这些机制各自的失效模式,而是多个机制交接的边界上,错误沿着因果链悄悄流到最意想不到的地方。Agent 框架的设计者必须系统性地理解这些机制,才能在每一个边界上埋入正确的防御,而不是等问题爆发后再打补丁。


学习完成于 2026-05-20

基于做 Agent 开发,有些大模型本身的底层机制,你不得不了解学习笔记整理

相关推荐
YDS8294 小时前
DeepSeek RAG&MCP + Agent智能体项目 —— RAG知识库的搭建和接口实现
java·ai·springboot·agent·rag·deepseek
counterxing4 小时前
我把 Codex 里的 Skills 做成了一个 MCP,还支持分享
前端·agent·ai编程
夜雪闻竹4 小时前
vectra 向量索引文件损坏怎么办
ai编程·向量·vectra
ZzT4 小时前
Harness 到底指什么
openai·ai编程·claude
宅小年4 小时前
AI 创业最危险的地方:太容易做出来
openai·ai编程·claude
麦客奥德彪5 小时前
Android Skills
架构·ai编程
言萧凡_CookieBoty5 小时前
一文讲清 RAG:让 AI 读懂业务知识库的核心方法
ai编程
冬奇Lab6 小时前
Agent 系列(一):Agent 是什么——不只是「会调工具的 LLM」
人工智能·llm·agent
kyriewen6 小时前
产品经理把PRD写成“天书”,我用AI半小时重写了一遍,他当场愣住
前端·ai编程·cursor