大模型底层机制与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 × 0.03 ≈ ∗ ∗ 0.03 ≈ ** 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 开发,有些大模型本身的底层机制,你不得不了解学习笔记整理

相关推荐
深蓝AI7 小时前
Claude Code 子智能体实战:让 AI 自己调 AI 来写代码
ai编程
ServBay7 小时前
Claude Code 被曝植入后门,AI 时代如何安全打造本地 DevOps
后端·ai编程·claude
DigitalOcean9 小时前
DigitalOcean 推出大模型自动化评估功能,上线前精准避坑
llm·agent
Fanta丶9 小时前
1.VibeCoding 终端命令基础使用
claude
Databend9 小时前
Agent 轨迹分析与归因的数据工程实践
大数据·数据库·agent
柒和远方9 小时前
Phase 7.2 RAG SafetyGuard:把用户上传资料当成低信任证据
aigc·agent
colir09 小时前
被粉丝夸爆的超级 ai 个人工作站,原来这么多福利
开源·agent·claude
程序员小假10 小时前
从问题到答案:RAG系统完整处理流程与核心机制深度拆解
后端·面试·agent
柒和远方10 小时前
Phase 6.8:把 KnowledgeDedupAgent / KnowledgeOrganizerAgent 补成可用闭环
agent
threerocks10 小时前
Fable + GPT Image = 无敌,Claude Code 中使用 Codex(订阅)生图的方案
aigc·ai编程