[{"content":"最近一个月，我一直在整理一类结构化 LLM 决策系统。它接收自然语言和结构化字段，在有限标签中做选择；证据不足时，系统要能转人工或者明确放弃自动判断。\n这篇只保留能公开讨论的方法，不涉及具体行业、业务字段、数据规模和实测数字。真正让我花时间的也不是某一句 Prompt，而是几个更基础的问题：候选从哪里来，怎样限制模型输出，第二次调用为什么可能和第一次不同，服务变快以后质量会不会发生变化。\n一、先把“生成答案”改成“受限决策” 开放式生成很灵活，直接放进业务流程却容易出问题：模型可能编出不存在的标签，也可能忽略方向、阶段之类的约束。即使返回了合法 JSON，字段值仍然可能选错。\n我更习惯把流程拆成下面这样：\n1 2 3 4 5 6 7 原始输入 -\u0026gt; 清洗和确定性规则 -\u0026gt; 检索候选 -\u0026gt; LLM 提取受控事实 -\u0026gt; 在候选 ID 中选择 -\u0026gt; 程序检查 + 必要时语义复核 -\u0026gt; 自动通过 / 人工复核 / 放弃自动判断 这类结构可以用在文档分类、工单路由、内容审核或其他高风险分类任务中。模型只负责一个边界清楚的局部判断：\n1 decision = model(facts, candidates, policy) facts 是当前样本中可验证的事实，candidates 是允许选择的集合，policy 描述决策边界。三者最好分开组织。全塞进一段自然语言里，后面很难知道模型到底忽略了哪一部分。\nJSON 合法，只解决了第一层问题 结构化输出有两种常见做法。一种是在 Prompt 里要求返回 JSON，解析失败后重试；另一种是在解码阶段按照 JSON Schema 屏蔽非法 token，也就是 constrained decoding。\nOpenAI 对 Structured Outputs 的说明提到，约束解码可以保证输出符合 schema，却不能保证对象里的值正确。一个答案可以格式完美，同时语义完全错误。\n所以校验至少要分成两层：\n1 2 结构校验：字段是否齐全、类型是否正确、枚举是否合法 领域校验：候选是否存在、事实是否冲突、决策是否越过业务边界 模型最好返回候选里的稳定 ID，不要重新生成标签名称。解释可以是一小段文本，真正参与程序路由的状态尽量使用枚举、布尔值和受控数组。\n还有一个常被漏掉的分支：模型拒绝回答，或者因为长度限制没有生成完整对象。结构化输出接口通常会单独暴露 refusal 和 finish reason，程序必须处理，不能把缺失字段补成一个看似正常的结果。\n二、检索的价值，是把问题变小 在分类系统里，检索的任务不只是补充知识。它还负责缩小模型的选择空间。\n环节 目标 常用观察指标 确定性规则 接住边界清楚的输入 覆盖率、冲突率 候选召回 让正确标签进入 top-k Recall@K 候选排序 把更相关的候选排在前面 MRR、NDCG LLM 决策 结合事实做最终选择 自动决策精度、放弃率 召回决定了系统的理论上限。正确答案没有进入候选池，后面的 LLM 再强也选不到。反过来，Recall@K 很高也不代表最终结果一定更好。候选太多会增加 token，相近标签之间也会互相干扰。\n候选数量还会影响上下文利用率。Lost in the Middle 发现，相关信息位于长上下文中部时，模型表现可能明显下降。“窗口装得下”和“模型用得好”并不是一回事。\n因此 top-k 应该被当作需要评测的超参数：\n1 2 3 k 太小：正确答案可能没有被召回 k 太大：上下文变长，候选彼此干扰 合适的 k：兼顾 Recall@K 和最终决策质量 BM25、Embedding 和 Reranker 适合解决不同的问题。BM25 对关键词、编号和专有名词敏感；Embedding 能处理同义表达；Reranker 在较小的候选集上做更细的 query-document 交互。常见做法是先用便宜的检索器召回，再把少量候选交给 Reranker 或 LLM。\n候选来源也值得保留。某个候选来自规则、稀疏检索还是向量检索，应该跟着结果进入 trace。误判发生时，这能帮助判断该调整召回还是决策节点。\n三、中间状态也是接口 多节点流程里，一个事实分析节点可能先把原文压缩成摘要，后续节点再根据摘要做判断。这样很方便，也很容易积累漂移。\n自然语言摘要是一种有损压缩。它可能省略否定和范围，也可能把推断写成事实。后续模型并不知道这句话来自原始输入还是上一个模型的猜测，只会把它当作新的上下文继续推理。\n更稳的中间协议可以写成：\n1 2 3 4 5 6 7 8 { \u0026#34;direction\u0026#34;: \u0026#34;outbound\u0026#34;, \u0026#34;entity_type\u0026#34;: \u0026#34;organization\u0026#34;, \u0026#34;stage\u0026#34;: \u0026#34;execution\u0026#34;, \u0026#34;has_supporting_document\u0026#34;: true, \u0026#34;evidence\u0026#34;: [\u0026#34;原文片段 A\u0026#34;, \u0026#34;输入字段 B\u0026#34;], \u0026#34;uncertain_fields\u0026#34;: [] } 这里最重要的不是字段叫什么，而是把“输入事实”“模型推断”和“不确定项”分开。自由文本仍然可以留在 reason 中供人阅读，但不要让它悄悄控制程序分支。\n从工程角度看，LLM 节点和普通服务没有区别：输入、输出、版本和失败行为都需要契约。模型生成的文字很顺，所以接口漂移有时比普通类型错误更难发现。\n四、Critic 能用，但要先告诉它查什么 多轮反思看起来很自然：先生成答案，再让模型审查，发现问题后重新生成。研究对这种方法给出了不同结果。\nSelf-Refine 在多种任务上观察到自反馈和迭代改写带来的提升；Large Language Models Cannot Self-Correct Reasoning Yet 则发现，没有外部反馈时，模型可能找不到自己的推理错误，修改后表现甚至会下降。\n这两个结论并不冲突。润色文本、补充遗漏，和定位一个未知的逻辑错误不是同一道题。Critic 如果只收到一句“请严格审查”，很可能重复第一次判断，也可能为了表现得更严格而修改原本正确的答案。\n我更认可有边界的审查：\n程序先检查 ID、枚举、互斥条件等硬约束。 规则无法判断时，再调用一次语义 Critic。 Critic 只检查预先定义的错误类型。 高风险状态变化需要独立证据或短确认。 达到调用上限后转人工，不允许无限反思。 Critic 真正需要的是外部锚点。程序规则、候选约束、错误位置和独立检索证据都算锚点。什么都不给，只让模型“再想想”，效果很难预测。\n还可以把“发现错误”和“修正错误”拆成两个节点。已有研究表明，模型可能具备修正已定位错误的能力，却不擅长自己找到错误位置。拆开后，每个节点的评测也更清楚。\n五、评测不能只留一个 accuracy 高风险分类中，不同错误的代价差很多。自动给出错误结果和把正确样本转给人工，不能算成同一种失败。\n一组更实用的指标包括：\n总体准确率； 自动决策精度和错误自动放行数； 人工复核率与错误放弃数； 最终状态分布； 各节点调用数、P50/P95 延迟、输入与输出 token； schema retry、服务 retry、超时和错误类型。 如果业务更重视避免错误自动决策，就应该给自动决策精度设置独立门槛，而不是让总体 accuracy 把它平均掉。可以把目标写成简单的成本函数：\n1 2 3 4 5 total_cost = false_accept * 高风险成本 + false_abstain * 机会成本 + manual_review * 人工成本 + latency * 服务成本 这些权重来自业务风险，不应由模型指标替代。\n全链路评测和冻结回放回答不同问题 全链路评测回答“这个版本能不能用”。冻结回放回答“变化从哪里产生”。\n冻结回放会保存某个节点的完整输入，固定 Prompt、候选和依赖，只替换模型或推理参数。如果冻结输入仍然漂移，问题更可能在模型或运行时；如果冻结后稳定、全链路却不稳定，就应检查上游状态。\n同一实验也需要重复运行。除了均值，还应观察最差轮次、方差和变化样本交集。一直失败的样本容易定位；偶尔正确、偶尔自信做错的样本更值得警惕。\n六、推理优化要分清 prefill 和 decode 一个 LLM 请求大致有两个阶段：\n1 2 prefill：处理整段输入，建立 KV cache 或模型状态 decode：逐 token 生成输出，过程更串行 上下文、候选和示例过长，主要增加 prefill；输出理由太长、反复 repair，主要增加 decode 和调用次数。优化前先拆开这些指标，不然很容易在错误的阶段花时间。\nPrefix cache 只节省共享前缀的 prefill vLLM 的 Automatic Prefix Caching 文档明确说明，APC 会复用相同前缀的 KV cache，从而跳过共享部分的 prefill，但不会减少新 token 的 decode 时间。\n它更适合长公共前缀、多轮对话，或者对同一文档的重复查询。如果瓶颈是长输出或重复调用，prefix cache 的帮助有限。\nPrompt 的排列也会影响缓存命中。稳定的系统指令、schema 和公共规则可以放在前面，动态事实与候选放在后面。不过这个建议仍需在具体运行时上验证。Prefix cache、推测解码、模型架构和并发调度可能互相影响，任何组合都不能只凭单项基准上线。\nMTP 和推测解码 普通自回归解码一次只确定一个 token。推测解码先草拟多个 token，再由目标模型一次验证。经典的 Speculative Decoding 使用接受/拒绝采样保持目标模型的输出分布，因此草拟模型不会直接替代目标模型。\nMTP 使用模型自带的 multi-token prediction head 预测后续 token。vLLM 的 MTP 文档也把它归在 speculative decoding 中。\n评估 MTP 时至少要记录：\n1 2 3 4 5 6 draft acceptance rate mean accepted length inter-token latency request throughput P50 / P95 latency 端到端质量指标 接受率高不等于服务一定更快。草拟和验证本身也有计算成本；高并发下，普通 decode 已经能组成较大的 batch，MTP 的相对收益可能缩小。\n并发不是越大越好 vLLM 使用 PagedAttention 和连续批处理提高显存利用率与吞吐。PagedAttention 论文 讨论了如何减少动态 KV cache 的碎片和重复保存，从而容纳更有效的 batch。\n但 batch 增大后，每一步 decode 会有更多序列竞争计算和内存带宽。max-num-seqs、客户端并发和吞吐之间通常不是单调关系，需要逐档压测。\n排查服务瓶颈时，可以先做这样的判断：\n1 2 3 4 队列高：检查调度容量、限流或服务副本 prefill 高：检查上下文、候选数量和前缀复用 decode 高：检查输出长度、MTP 和重复调用 GPU 不忙但流程慢：检查网络、串行节点和任务编排 GPU 利用率本身说明不了请求是否高效。它需要和队列、延迟、吞吐、KV cache 使用量一起看。\n七、一次结果需要保存哪些版本 模型版本只是复现条件的一部分。结果还会受到检索索引、标签集合、参考数据、策略配置、Prompt、采样参数和运行时版本影响。\n对外部依赖，可以先规范化 JSON，再计算内容哈希；一次任务只引用不可变的快照 ID 和哈希，不在执行中途重新获取可能变化的数据。\n我现在会把一次 LLM 实验看成下面这个元组：\n1 2 3 4 5 6 7 8 9 run = ( code_commit, model_and_runtime, prompt_version, decoding_config, dependency_snapshot, evaluation_dataset, random_and_request_metadata ) 少了其中一项，复现失败时都可能说不清原因。即使温度设为 0，也不要假设系统绝对确定。并行归约、批处理顺序、服务版本和上游自然语言状态仍可能产生差异。\n运行配置可以进入版本控制，密钥不可以。可复现实验和 secret 管理是两件事。\n八、通用 Prompt 不应该保存本地政策 领域示例经常不只是在教模型怎样推理，还可能暗中携带业务政策。直接删掉示例，模型失去的也许不是语言能力，而是决策边界。\n更清楚的拆法是：\n1 2 3 通用推理层：怎样阅读事实、比较候选、处理冲突 领域知识层：常见概念、关系和表达方式 本地政策层：允许的边界、例外规则和风险阈值 推理方法可以复用，政策应显式配置和版本化。所谓泛化，也需要在新的标签集合、独立标注和不同输入分布上验证。把特定词汇从 Prompt 里删掉，只能说明文本变得更抽象，不能说明系统更通用。\n以后再做类似系统，我会先检查这些 正确答案不在候选里时，系统是否会明确停止？ 模型返回的是稳定 ID，还是重新生成标签名称？ schema 合法后，是否还有领域校验？ 中间节点传递的是受控状态，还是会漂移的摘要？ Critic 是否知道具体要检查哪类错误？ 自动做错和转人工是否使用不同门槛？ 能否冻结任意节点输入，单独回放模型与运行时？ 性能数据是否拆开 queue、prefill、decode 和调用次数？ 每次实验能否还原代码、模型、Prompt、依赖和评测集？ 通用方案是否在新的数据分布上验证过？ 一个月前，我更关注模型能不能给出正确答案。现在我更在意，当模型给出答案时，系统有没有足够证据决定自动接受、转给人工，或者承认信息不足。这个问题没什么神秘的，却决定了 LLM 能不能进入真实流程。\n延伸阅读 Introducing Structured Outputs in the API Lost in the Middle: How Language Models Use Long Contexts Self-Refine: Iterative Refinement with Self-Feedback Large Language Models Cannot Self-Correct Reasoning Yet Fast Inference from Transformers via Speculative Decoding vLLM Automatic Prefix Caching Efficient Memory Management for Large Language Model Serving with PagedAttention ","date":"2026-07-24T00:00:00Z","permalink":"/p/structured-llm-decision-system-review/","title":"从 Prompt 到可审计系统：结构化 LLM 决策的工程笔记"},{"content":"最近在补 RAG 和检索系统的基础。刚开始看资料时，最容易混在一起的几个词就是：BM25、Embedding、SPLADE、Reranker，再加一个听起来很工程的词：多路召回。\n单独看定义不难，难点在于把它们放进同一条检索链路里。复习时重点回答这些问题：\n哪些方法负责“先找一批候选”？ 哪些方法负责“把候选重新排好”？ BM25 和 Embedding 明明都能搜，为什么还要一起用？ Reranker 听起来更准，为什么不直接全库跑？ SPLADE 到底是关键词检索，还是语义检索？ 这篇笔记不铺满公式，重点讲清每种方法在检索系统里的位置、优势和边界。\n速读版 先给一个压缩版结论：\n方法 系统位置 主要解决 优点 短板 BM25 关键词搜索 字面精确命中 快、可解释、对术语敏感 不懂同义表达 Embedding 语义近邻搜索 意思接近但字面不同 泛化强，适合自然语言问题 细粒度容易混 SPLADE 神经稀疏搜索 语义扩展后的关键词匹配 兼顾稀疏结构和语义扩展 模型和索引更复杂 Reranker 精排裁判 少量候选谁最相关 判断细，通常更准 慢，不适合全库扫描 一条典型链路大概是：\n1 2 3 4 5 用户 query -\u0026gt; BM25 / Embedding / SPLADE 多路召回 -\u0026gt; 候选合并、去重、分数融合 -\u0026gt; Reranker 精排 -\u0026gt; 返回 top-k 或交给下游生成模型 多路召回要解决的是：不要让单一检索方法承担所有召回责任，而是让不同方法互相补盲区。\n一、先分清召回和精排 搜索系统一般不会一开始就让最复杂的模型扫描整个文档库。文档量一大，成本会很高。\n所以它会分成两步：\n1 2 3 4 5 Recall 召回 从大量文档里快速捞出一批“可能相关”的候选。 Ranking / Reranking 排序或精排 在候选集合变小之后，再认真比较谁更相关。 召回阶段的目标是“尽量别漏掉相关候选”；精排阶段的目标是“在候选里排得更准”。\nBM25、Embedding、SPLADE 通常更适合放在召回阶段；Reranker 通常更适合放在精排阶段。\n二、BM25：最经典的关键词检索 BM25 是传统搜索里很经典的方法。它不理解深层语义，也不做推理，主要看一个问题：\nquery 里的词，在文档里有没有出现？出现的词有没有区分度？\n比如用户搜：\n1 酒店住宿报销 如果一篇文档里出现了“酒店”“住宿”“报销”，它就应该比完全没出现这些词的文档更相关。\nBM25 可以先记成：\n1 BM25 分数 ≈ 词频 TF × 逆文档频率 IDF × 文档长度修正 复习时先理解这三个部分，再回到完整公式。\n2.1 词频：命中 query 词，说明可能相关 如果 query 里有“住宿”，文档 A 出现了“住宿”，文档 B 没出现，那文档 A 大概率更相关。\n但 BM25 不会让词频无限加分。一个词重复出现很多次，并不代表相关性可以无限升高。所以 BM25 会对词频做饱和处理。\n2.2 IDF：越少见的词，越有区分度 有些词太常见：\n1 申请、说明、费用、管理、流程 它们出现了当然有一点信息，但区分度不强。\n有些词更具体：\n1 住宿、发票、差旅、押金、手续费 这些词更能帮助判断文档主题。\n这就是 IDF 的直觉：一个词在全库里越少见，它越能区分文档。\n2.3 文档长度修正：长文档不能天然占便宜 长文档词多，命中 query 词的概率天然更高。如果不修正，长文档会占便宜。\nBM25 会考虑文档长度，让“短但精准命中”的文档也有机会排在前面。\n2.4 BM25 的位置 BM25 特别适合这些场景：\n搜索专有名词 搜索编号、代码、实体名 搜索业务词或固定术语 需要可解释的检索结果 比如：\n1 增值税专用发票 如果文档里明确出现这个词，BM25 的表现通常很稳。\n它的短板也很明显：不太懂同义词。\n1 2 3 住酒店 住宿费 差旅住宿支出 这些表达意思接近，但字面不完全一样。如果没有共同词，BM25 可能就会漏。\n复习锚点：BM25 适合作为基础召回，尤其适合术语、编号、实体名；短板是同义表达和口语化改写。\n三、Embedding：把文本放进语义空间 Embedding 的思路和 BM25 不同。BM25 看词有没有命中；Embedding 把文本编码成向量。\n语义相近的文本，在向量空间里应该更接近。\n比如：\n1 2 3 员工出差住酒店 差旅住宿费用 酒店房费报销 这些句子字面不同，但语义接近。好的 Embedding 模型会尽量把它们编码到相近的位置。\n检索时通常是这样：\n1 2 3 4 文档库里的文档 -\u0026gt; 提前编码成向量 用户 query -\u0026gt; 编码成向量 计算 query 向量和文档向量的相似度 返回最相似的 top-k 文档 最常见的相似度是余弦相似度：\n1 cosine similarity = 两个向量方向的接近程度 它关心的是方向，而不是文本长度。两个文本语义越接近，向量夹角通常越小，相似度越高。\n3.1 Embedding 解决了 BM25 的一部分盲区 用户的问题往往很口语化：\n1 出差住酒店怎么报销 文档标题可能是：\n1 差旅住宿费用管理办法 BM25 如果没有命中关键分词，可能不够稳；Embedding 更可能从整体意思上判断它们相关。\nEmbedding 适合：\n同义表达 口语化 query 标题和问题表达方式不一致 需要整体语义理解的场景 3.2 Embedding 也会犯“语义太近”的错 Embedding 的问题是，它有时会太模糊。\n比如：\n1 2 3 4 技术服务费 咨询服务费 软件服务费 会议服务费 从语义上看都和“服务”有关，向量空间里可能靠得很近。但在实际业务里，它们可能需要被分到不同类别。\n复习锚点：Embedding 擅长找“语义相近”的内容，但在术语边界、细分类别、编号实体上可能不够精确。因此它常和 BM25 搭配使用。\n四、为什么需要多路召回 BM25 和 Embedding 的优缺点互补。BM25 负责词面命中，Embedding 负责语义相近；SPLADE 可以补充神经稀疏召回。多路召回就是同时跑多种召回方法，再合并候选：\n1 2 3 4 5 6 BM25 召回一批 Embedding 召回一批 SPLADE 也可以召回一批 -\u0026gt; 合并候选 -\u0026gt; 去重 -\u0026gt; 融合或重排 举个简单例子。\n1 query: 出差住酒店怎么报销 BM25 可能找到：\n1 2 酒店发票填写要求 住宿费报销单填写说明 Embedding 可能找到：\n1 2 差旅费用管理办法 员工出差报销流程 两路结果合起来，比单独用一路更稳。\n4.1 分数不能直接相加 多路召回带来一个麻烦：不同方法的分数不是一个尺度。\nBM25 可能返回：\n1 12.7, 8.3, 3.1 Embedding 的余弦相似度可能是：\n1 0.82, 0.77, 0.65 这些分数不能直接相加。\n常见处理方法有两类。\n第一类是分数归一化，例如 min-max normalization，把不同来源的分数压到相近区间，再加权融合：\n1 final_score = 0.5 * norm(BM25) + 0.5 * norm(Embedding) 第二类是只看排名，比如 Reciprocal Rank Fusion。它不太关心原始分数，而是看一个文档在每一路里排第几：\n1 RRF(d) = sum(1 / (k + rank_i(d))) 如果一个文档在多路召回里都排得靠前，它的融合分数就会更高。\nRRF 的复习锚点：多路召回都排得靠前的文档，融合后应该获得更高优先级。\n五、Reranker：候选少了以后，再认真判断 如果 BM25、Embedding、SPLADE 负责先找候选，Reranker 负责在候选变少后做细粒度比较。\nEmbedding 通常是 bi-encoder：\n1 2 3 query 单独编码 doc 单独编码 然后算向量相似度 这种方式快，因为文档向量可以提前算好。\nReranker 通常接近 cross-encoder 思路：\n1 2 把 query 和 doc 放在一起 让模型直接判断相关性 例如：\n1 2 3 query: 出差住酒店怎么报销 doc: 差旅住宿费用管理办法 score: 0.93 模型可以同时看到 query 和 doc，细粒度判断它们是否匹配。\n5.1 为什么 Reranker 不直接全库跑 Reranker 通常更准，但也更慢。\n如果文档库有 100 万篇，让 Reranker 对每篇都判断一次：\n1 2 3 4 query-doc1 query-doc2 ... query-doc1000000 成本太高。\n所以它一般放在召回之后：\n1 2 先用 BM25 / Embedding / SPLADE 召回 top50 再用 Reranker 对这 50 个候选重新排序 复习锚点：召回阶段追求快和覆盖，Reranker 阶段追求细粒度相关性判断。\n5.2 Reranker 适合处理候选之间的细微差别 比如 query 是：\n1 酒店发票抬头填错了怎么办 召回阶段可能找出：\n1 2 3 酒店发票填写要求 发票作废与重开流程 差旅住宿费报销规则 这些都相关，但最该排前面的可能是“发票作废与重开流程”。Reranker 就是在这种时候发挥价值。\n六、SPLADE：神经网络版的关键词检索 SPLADE 是这几个方法里最容易混淆的一块，可以先这样理解：\nSPLADE 试图把 BM25 的稀疏可解释性，和神经模型的语义扩展能力接起来。\nBM25 在词项空间工作。文档里有哪些词，就在哪些词上有统计权重。\nEmbedding 在稠密向量空间工作。每个维度不一定有明确含义，但整体能表达语义。\nSPLADE 也在词表空间里工作。假设模型词表有 30,000 个 token，那么一段文本会被表示成一个 30,000 维向量。\n不同的是，这个向量很稀疏：\n1 2 大部分维度是 0 少数相关 token 维度有正权重 比如用户输入：\n1 住酒店 SPLADE 可能不只激活“酒店”，还会激活：\n1 住宿、旅馆、房费、差旅 这相当于模型自动帮 query 做语义扩展，但结果仍然落在稀疏词项空间里。\n6.1 SPLADE 的激活直觉 很多 SPLADE 实现会对模型输出做类似这样的变换：\n1 log(1 + ReLU(x)) 拆开看：\nReLU(x)：负数清零，只保留正向激活。 log(1 + x)：压缩过大的权重，让数值更稳定。 这样得到的是非负稀疏权重。\n检索时可以直接做点积：\n1 score(query, doc) = sparse_query_vector · sparse_doc_vector 如果 query 和 doc 在相同或相关 token 上都有权重，分数就会变高。\n6.2 SPLADE 放在哪个位置 可以把它放在 BM25 和 Embedding 中间理解：\n1 2 3 BM25 ：词面匹配，稀疏，可解释 Embedding ：语义匹配，稠密，不太可解释 SPLADE ：神经稀疏匹配，有语义扩展，也保留词项空间 SPLADE 吸引人的地方在于，它不像普通 Embedding 那样完全进入黑盒稠密空间，也不像 BM25 那样只依赖原始词面。它可以学习到一些扩展关系。\n它的成本也更高：需要专门模型，向量维度很高，索引和部署比 BM25 更复杂。\n七、一个完整检索链路可以怎么想 假设用户问：\n1 差旅住宿发票丢了还能报销吗 一条比较完整的检索链路可以这样组织：\n1 2 3 4 5 6 7 1. BM25 找到包含“差旅”“住宿”“发票”“报销”的文档 2. Embedding 找到语义上接近“发票遗失”“报销材料补充”的文档 3. SPLADE 如果可用，再补一批神经稀疏召回结果 4. 合并候选，去掉重复文档 5. 用归一化加权或 RRF 做候选融合 6. 用 Reranker 对候选重新排序 7. 把 top 文档交给下游问答模型或直接返回 每一步对应一个具体问题：\nBM25：有没有明确命中关键词？ Embedding：语义上像不像？ SPLADE：能不能做更聪明的词项扩展？ Reranker：候选里谁最贴合用户问题？ 八、选型建议 如果只是做一个小型知识库，最简单的起点通常是：\n1 Embedding 召回 + Reranker 精排 如果业务词很强，比如法规条款、财务科目、商品型号、错误代码，可以加上 BM25：\n1 BM25 + Embedding 多路召回 + Reranker 精排 如果发现 BM25 词面太死、Embedding 又容易把细分类别混在一起，再考虑 SPLADE：\n1 BM25 + Embedding + SPLADE 多路召回 + Reranker 精排 不要一开始就把所有东西都加上。检索系统越堆越复杂，调试成本也越高。更稳的做法是先跑通 baseline，再根据错误样本决定补哪一路。\n小结 多路召回不是一个单独算法，而是一种系统设计思路：让不同检索方法从不同角度找候选，再用排序模型做最终筛选。\nBM25 解决精确词面匹配，Embedding 解决语义泛化，SPLADE 尝试在稀疏词项空间里加入神经语义扩展，Reranker 负责最后的细粒度判断。\n以后复习或设计检索链路时，优先问这些问题：\n1 2 3 4 5 6 它用了哪些召回路？ 每一路分别解决什么盲区？ 候选是取并集，还是只在某一路结果里重打分？ 不同分数怎么融合？ Reranker 放在多少候选之后？ 最终阈值有没有单独校准？ 这些问题比单独背概念更有用。检索效果通常不是由某个单点算法决定的，而是由整条链路的召回覆盖、候选融合、精排质量和阈值校准共同决定的。\n","date":"2026-05-15T00:00:00Z","permalink":"/p/multi-recall-retrieval-notes/","title":"多路召回学习笔记：BM25、Embedding、SPLADE 和 Reranker 各在解决什么"},{"content":"第七章的主题很集中：大模型进入应用场景后，先要评估能力边界，再接入外部知识，最后才谈工具调用和任务执行。\n这一章可以按三个模块复习：评测、RAG 和 Agent。评测判断模型在哪些任务上可靠；RAG 把外部文档接入生成过程；Agent 让模型通过工具参与行动。\n整章可以整理成下面这条应用链路：\n1 2 3 4 5 6 7 8 评测 Evaluation -\u0026gt; 判断模型在哪些任务上可靠，边界在哪里 RAG Retrieval-Augmented Generation -\u0026gt; 把外部文档和实时知识接入生成过程 Agent -\u0026gt; 让模型通过工具调用参与更复杂的任务流 这一章可以先按三层记：能力边界、知识接入、工具行动。\n一、先从评测开始：不要只问模型强不强 进入应用层后，最容易犯的错误是把模型能力说得太笼统：一个模型在榜单上分数高，不代表它能胜任所有场景。\n做应用时，要把“强不强”拆成具体能力问题：\n它是否懂通用知识 它数学推理是否稳定 它能不能处理长文本 它多语言能力怎样 它是否会正确使用工具 它在金融、法律、医疗等垂直领域是否可靠 所以第七章把评测集按能力拆开看，比如 MMLU 偏通用多任务理解，GSM8K/MATH 偏数学推理，ARC/GPQA/HellaSwag 偏推理和常识，InfiniteBench/Multi-needle 偏长文本，MGSM 偏多语言数学。\n评测是在给应用选择画边界。\n比如我要做知识问答系统，就不能只看聊天榜单，还要看事实性、长文档理解和检索后回答是否忠实。我要做工具型助手，就不能只看语言流畅度，还要关心工具调用正确率、参数是否填对、失败时能不能恢复。\n复习锚点：评测不是为了得到一个总分，而是为了判断模型适合放进哪个场景，以及哪些能力不能靠感觉判断。\n二、RAG：把模型从“只靠参数记忆”拉回到外部知识 第七章第二部分进入 RAG。这里的背景很直接：LLM 虽然会生成，但它有几个天然问题：\n训练数据可能过时 专业领域知识不一定充分 输出可能产生幻觉 很难直接给出可追溯依据 RAG 的思路很直接：生成之前先检索。用户提出问题后，系统先从文档库里找相关片段，再把这些片段作为上下文交给模型，让模型基于外部材料回答。\nRAG 的主链路可以写成：\n1 2 3 文档 -\u0026gt; 切分 -\u0026gt; 向量化 -\u0026gt; 向量库 | 用户问题 -\u0026gt; 向量化 -\u0026gt; 相似度检索 -\u0026gt; 上下文拼接 -\u0026gt; LLM 生成回答 RAG 把知识系统拆成两个部分：\n模型负责理解问题、组织语言和生成答案 检索系统负责把当前问题最需要的外部证据找出来 两者配合以后，应用可以把最新资料、私有文档、领域手册接到生成链路里，减轻对模型参数记忆的依赖。\n图：RAG 先检索相关上下文，再让模型基于上下文生成回答\n三、Tiny-RAG：最小实现里要盯住的几个模块 第七章还写了一个 Tiny-RAG。这个实现很适合学习，因为它把 RAG 系统拆成了几个足够小的部件：\n文档加载与切分：ReadFiles 向量化：BaseEmbeddings / OpenAIEmbedding 向量存储与检索：VectorStore 生成模型封装：OpenAIChat 演示入口：demo.py 图：Tiny-RAG 把 RAG 拆成文档、向量、检索和模型回答几个最小模块\n3.1 文档切分决定证据粒度 文档加载部分支持 pdf/md/txt，读出来之后会按 token 长度切成 chunk。这里要记两个参数：\n1 2 max_token_len = 600 cover_content = 150 max_token_len 控制每个片段不要太长，cover_content 则给相邻片段保留重叠内容。这个重叠很重要，因为很多语义边界不会刚好落在切分点上。如果完全硬切，检索时很容易丢掉上下文。\n所以 chunk 策略本质上是在做取舍：\nchunk 太短，语义不完整 chunk 太长，检索不精确，而且占上下文 重叠太少，边界信息容易断 重叠太多，索引膨胀，成本增加 复习锚点：chunk 策略表面上是数据预处理，实际是在决定模型后面能看到什么证据。\n3.2 向量化层要抽象出来 第七章先定义 BaseEmbeddings，再写 OpenAIEmbedding。这个设计很朴素，但教学价值很清楚：embedding 模型应该作为可替换模块接入 RAG。\n1 2 3 4 5 6 7 class BaseEmbeddings: def get_embedding(self, text: str, model: str): raise NotImplementedError @classmethod def cosine_similarity(cls, vector1, vector2): ... 这里的重点在于接口抽象：embedding 模型被包装成了可替换模块。以后如果从 API embedding 换成本地 embedding，RAG 主流程不用大改，只要新的类实现 get_embedding 就行。\n复习锚点：RAG 里的模型不止最终生成回答的 LLM，还有负责把文本映射到向量空间的 embedding 模型。embedding 质量不好，LLM 再强也可能拿不到正确上下文。\n3.3 向量库的最小检索逻辑就是相似度排序 Tiny-RAG 的 VectorStore 很轻量：把文档片段和向量保存下来，查询时计算问题向量和每个文档向量的相似度，然后取 Top-k。\n1 2 3 4 5 6 7 def query(self, query, EmbeddingModel, k=1): query_vector = EmbeddingModel.get_embedding(query) scores = [ self.get_similarity(query_vector, vector) for vector in self.vectors ] return np.array(self.document)[np.argsort(scores)[-k:][::-1]].tolist() 这个版本属于教学级向量数据库，它把最基本的动作讲明白了：\n问题也要向量化 问题向量和文档向量进入同一个空间比较 相似度最高的片段被取出来作为上下文 后续做项目时，可以把这里替换成 FAISS、Milvus、Qdrant、Chroma 之类的向量库，也可以补 reranker。但无论外壳怎么换，目标都还是“找到和问题最相关的上下文”。\n3.4 RAG prompt 要约束模型只基于上下文回答 最后，OpenAIChat 会把检索到的内容放进一个 RAG 专用 prompt。这个 prompt 要明确告诉模型：\n使用给定上下文回答 不知道就说不知道 上下文不足时不要编 用中文回答 这个 prompt 给模型增加“证据约束”：答案应建立在可追溯材料上。RAG 做得好不好，要回到评测中看事实一致性、引用准确性和答案忠实性。\n四、RAG 的学习收获：系统质量不只取决于 LLM RAG 系统的质量，很大一部分取决于生成模型之外的环节。\n如果答案错了，可能原因很多：\n原始文档质量不好 切分粒度不合适 embedding 模型不匹配 Top-k 召回错了 prompt 没有限制模型 模型没有正确使用上下文 这和直接问 LLM 很不一样。直接问模型时，问题通常被归因到“模型会不会”。但到了 RAG，系统多了检索层、索引层、上下文构造层，错误也会分散到更多地方。\n所以看 RAG 系统时，不要只问“接了什么大模型”，还要问：\n文档怎么切 向量怎么建 检索怎么排 上下文怎么塞 回答怎么评 这几个问题答清楚，才算开始理解 RAG 的系统质量。\n五、Agent：从“会回答”走向“会调用工具” 第七章最后讲 Agent。它先给了一个比较完整的定义：LLM Agent 以 LLM 为中心，结合目标理解、规划、记忆、工具使用、反思迭代等能力，去完成更复杂的任务。\nAgent 可以整理成一句话：\nAgent 是把 LLM 放进一个可行动的任务系统里。\n普通聊天模型的输出主要是文字。Agent 的变化在于，模型可以在合适的时候选择工具，比如查时间、搜索百科、查询天气、做计算，然后把工具结果继续放回对话，让模型生成最终回答。\nAgent 改变了 LLM 的角色：模型不只是生成文本，还参与调度工具。它需要承担这些职责：\n判断用户到底要什么 判断是否需要外部工具 选择正确工具 填好工具参数 读取工具返回结果 继续生成自然语言回复 第七章也把 Agent 粗略分成任务导向型、规划推理型、多 Agent 系统、探索学习型几类。当前复习可以先抓任务导向型，因为 Tiny-Agent 主要演示的就是这条最小链路。\n六、Tiny-Agent：tool calling 的最小闭环 Tiny-Agent 的实现围绕 OpenAI 兼容接口里的 tool_calls 展开。它有三个部件：\n工具函数 工具 schema 转换 Agent 对话与工具调用循环 工具函数本身很普通，比如获取时间、搜索 Wikipedia、查询天气。但在 Agent 里，普通函数要变成模型可理解的工具，就必须有清晰的函数名、参数类型和 docstring。\n1 2 3 4 5 def get_current_datetime() -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34; 获取真实的当前日期和时间。 \u0026#34;\u0026#34;\u0026#34; ... 接着，function_to_json 会读取函数签名，把它转换成 tool schema：\n1 2 3 4 5 6 7 8 9 10 11 def function_to_json(func) -\u0026gt; dict: signature = inspect.signature(func) ... return { \u0026#34;type\u0026#34;: \u0026#34;function\u0026#34;, \u0026#34;function\u0026#34;: { \u0026#34;name\u0026#34;: func.__name__, \u0026#34;description\u0026#34;: func.__doc__ or \u0026#34;\u0026#34;, \u0026#34;parameters\u0026#34;: ... }, } 模型看到的是结构化 schema。工具描述不清楚、参数设计不合理，模型就更容易选错工具或填错参数。\n图：Tiny-Agent 的流程是模型提出工具调用，程序执行工具，再把结果交回模型\n6.1 Agent 类负责维护调用循环 Agent.get_completion 的流程可以简化成这样：\n1 2 3 4 5 6 7 8 9 用户输入 -\u0026gt; 追加到 messages -\u0026gt; 调用模型并传入 tools -\u0026gt; 如果模型返回 tool_calls -\u0026gt; 执行对应工具 -\u0026gt; 把工具结果追加为 tool message -\u0026gt; 再调用模型 -\u0026gt; 保存 assistant 回复 -\u0026gt; 返回最终答案 这个循环说明了 tool calling 的边界：模型输出结构化调用请求，外层程序执行工具，工具结果再回到模型上下文中。\n所以 Agent 的可靠性同时取决于模型和外层控制逻辑：\n工具是否有白名单 参数是否校验 调用失败怎么恢复 工具返回是否可信 历史消息是否会越积越乱 这些问题在纯聊天模型里不太显眼，但一旦模型开始调用工具，就必须认真处理。\n6.2 Tiny-Agent 很适合作为 Agent 入门 第七章前面介绍了规划、记忆、反思等能力，但配套 Tiny-Agent 主要还是一个 tool calling demo。它有工具、有消息历史、有二次模型调用，但还没有实现复杂规划、长期记忆或反思循环。\n作为学习入口，它很合适，因为它先把基础接口关系讲清楚了：\n工具如何暴露给模型 模型如何提出工具调用 程序如何执行工具 工具结果如何回到模型上下文 等这一层明白之后，再看 ReAct、多 Agent、长期记忆、任务规划，才不会只停在概念名词上。\n工程边界也要记住：Tiny-Agent 中用 eval(...) 执行工具调用，适合教学演示；真实系统应该换成更安全的白名单分发。模型一旦能触发工具，权限和安全边界就必须认真处理。\n七、这一章留给我的应用层框架 这一章最适合留下一个应用层框架：\n1 2 3 4 5 6 7 8 评测 -\u0026gt; 明确模型能力边界 RAG -\u0026gt; 接入外部知识与可追溯证据 Agent -\u0026gt; 接入工具和任务执行过程 这里有三个复习结论：\n评测要看具体任务边界\n榜单分数只能提供参考，应用还要关心场景、数据、语言、工具和安全边界。\nRAG 要把知识放到可更新的位置\n外部文档、向量索引和检索结果共同决定模型能看到哪些依据。\nAgent 要把工具调用纳入控制流程\nAgent 让模型不仅能回答，还能通过工具查询、计算、搜索和执行任务。\n第七章适合作为应用入口。Tiny-RAG 和 Tiny-Agent 给出了最小可运行骨架，帮助先看清系统边界。\n八、写在最后：应用层要把不可靠变得可控 大模型应用不能只盯着模型本身，还要补上评测、检索、工具、日志和控制边界。\n评测告诉我模型哪里能用，RAG 让回答尽量有依据，Agent 让模型可以接入行动接口。目标很实际：让模型在真实场景里更可控。\n如果只保留这一章的学习结论，可以记住三句话：\n评测负责应用边界。 RAG 负责外部知识接口。 Agent 负责工具调度。 复习应用层时，关注点要落到三件事：系统怎样设计、怎样观测、怎样迭代。\n参考资料 Datawhale. Happy-LLM：第七章《大模型应用》 Datawhale Happy-LLM 仓库 Retrieval-Augmented Generation for Large Language Models: A Survey ","date":"2026-05-02T00:00:00Z","image":"/p/happy-llm-llm-applications-rag-agent/cover.png","permalink":"/p/happy-llm-llm-applications-rag-agent/","title":"大模型应用这一层：从评测、RAG 到 Agent"},{"content":"第六章开始贴近工程实践：用 Transformers + datasets + Trainer + DeepSpeed + peft 这套生态把 LLM 训练流程串起来。\n这一章可以整理成一句话：\n模型怎么初始化、数据怎么整理进 Trainer、预训练和 SFT 的标签怎么分开、资源不够时怎么换成 LoRA，这四件事就是第六章的主线。\n这篇文章按训练流程整理，方便以后回查每一步对应的框架、数据格式和参数。\n一、先把第六章的训练流程整理成链路 从工程视角看，第六章讲的是这样一条链路：\n1 2 3 4 5 6 模型配置与 tokenizer -\u0026gt; 预训练数据处理 -\u0026gt; TrainingArguments + Trainer -\u0026gt; DeepSpeed 分布式启动 -\u0026gt; SFT 数据构造 -\u0026gt; LoRA / peft 高效微调 这条链路要看的不是框架名字有多熟，而是每个组件承担哪一层职责：\nTransformers 负责把模型和训练流程抽象成统一接口 datasets 负责把原始文本或对话数据整理成模型可训练的张量格式 Trainer 负责承接训练参数、数据集、保存和日志等通用逻辑 DeepSpeed 负责把训练推到多卡和更大规模上 peft 负责在资源受限时把全量微调切成 LoRA 这种高效微调路径 复习锚点：Transformers 管模型接口，datasets 管数据流水线，Trainer 管训练脚手架，DeepSpeed 管大规模训练，peft 管高效微调。\n图：第六章借助 Hugging Face 生态把模型、数据集与训练框架串到了一起\n二、第一步：先把模型和 tokenizer 初始化起来 模型和 tokenizer 初始化负责明确两件事：训练哪个模型，以及文本如何进入这个模型。\n第六章选的是 Qwen-2.5-1.5B。Transformers 里有两种常见初始化方式：\n基于配置从零初始化一个模型 基于已有权重加载一个预训练模型 对应到代码里，主干大概就是下面这样：\n1 2 3 4 5 6 from transformers import AutoConfig, AutoModelForCausalLM, AutoTokenizer config = AutoConfig.from_pretrained(model_path) model = AutoModelForCausalLM.from_config(config, trust_remote_code=True) tokenizer = AutoTokenizer.from_pretrained(model_path) 如果要直接继承已有权重，通常会改成：\n1 model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True) 2.1 Auto* 这套接口是在接住“模型族差异” 只要配置和权重兼容，AutoConfig、AutoModelForCausalLM、AutoTokenizer 就能把很多模型族差异统一到同一套接口下。这样训练脚本可以把重心放在数据、参数和流程上。\n2.2 训练阶段不同，模型加载方式可能不同，但 tokenizer 契约不能乱 无论是 Pretrain 还是 SFT，tokenizer 都是一套输入协议。它定义的是：\n词表怎么映射 特殊 token 怎么表示 对话边界怎么标记 padding 和 EOS 怎么约定 尤其到了后面的 SFT，这个 tokenizer 契约会直接决定 chat template 和 labels 的构造方式。\n复习锚点：模型初始化把后续训练步骤接到统一的 Hugging Face 模型接口上；tokenizer 则规定了输入协议。\n三、第二步：把预训练数据整理成 Trainer 能直接吃的样子 预训练数据处理负责把原始文本变成 CLM 训练样本。\n第六章的预训练流程是 Causal Language Modeling，也就是预测下一个 token。进入 datasets 生态后，数据预处理通常写成流水线：\n用 load_dataset 读入原始 JSON/JSONL 用 tokenizer 把文本转成 token IDs 把多个文本段拼起来，切成固定长度 block 令 labels = input_ids.copy() 这段代码骨架最值得回看：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from datasets import load_dataset from itertools import chain ds = load_dataset(\u0026#34;json\u0026#34;, data_files=train_file) def tokenize_function(examples): return tokenizer([item for item in examples[\u0026#34;text\u0026#34;]]) tokenized_datasets = ds.map( tokenize_function, batched=True, remove_columns=column_names, ) def group_texts(examples): concatenated_examples = {k: list(chain(*examples[k])) for k in examples.keys()} total_length = len(concatenated_examples[list(examples.keys())[0]]) total_length = (total_length // block_size) * block_size result = { k: [t[i:i + block_size] for i in range(0, total_length, block_size)] for k, t in concatenated_examples.items() } result[\u0026#34;labels\u0026#34;] = result[\u0026#34;input_ids\u0026#34;].copy() return result 3.1 labels = input_ids.copy() 是预训练阶段的记忆锚点 这句代码明确了：Pretrain 阶段的监督目标就是整段文本自身。\n也就是说，预训练数据的重点不在“问题-答案”格式，而在：\n先把文本稳定 tokenize 再拼接成足够长的训练块 让每个块都作为一个 CLM 样本参与训练 group_texts 的作用是把多个文本片段拼接后切成固定长度 block。预训练更关注吞吐和有效 token 利用率，而不是保留每段原始文本的天然边界。固定长度 block 可以减少 padding 浪费。\n3.2 预训练数据处理是在整理训练协议 进入框架训练后，数据处理要回答四个问题：\nblock 多长 是否拼接多个样本 labels 怎样和 input_ids 对齐 attention_mask 由谁生成 这些细节表面上是数据预处理，实际上已经在决定模型训练看到的输入分布。\n复习锚点：预训练数据处理就是把“文本 -\u0026gt; token -\u0026gt; block -\u0026gt; labels”这条链放进 datasets.map。\n四、第三步：用 Trainer 把训练参数、日志和保存逻辑接起来 Trainer 这一层负责把模型、数据和训练参数装进一个可运行的训练器。\n第六章用的是 TrainingArguments + Trainer 这条最典型的 Transformers 路线。骨架很简单：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from transformers import TrainingArguments, Trainer, default_data_collator training_args = TrainingArguments( output_dir=\u0026#34;output\u0026#34;, per_device_train_batch_size=4, gradient_accumulation_steps=4, logging_steps=10, num_train_epochs=1, save_steps=100, learning_rate=1e-4, gradient_checkpointing=True, ) trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, tokenizer=tokenizer, data_collator=default_data_collator, ) 4.1 TrainingArguments 负责的是“训练策略的通用外壳” 比如：\nbatch size 梯度累积 学习率 日志频率 保存策略 gradient checkpointing 这些参数放进 TrainingArguments 后，模型逻辑、数据逻辑和训练策略开始分层。\n4.2 Trainer 统一的是训练脚手架 到了这里，很多通用工作都不需要自己再反复写：\n前向和反向训练循环 日志打印 checkpoint 保存 tokenizer/data collator 接入 分布式训练接口对接 复习锚点：Trainer 的价值不是替代理解，而是把已经理解的训练骨架抽成可复用模板。\n五、第四步：从 notebook 走向可长期运行的 DeepSpeed 训练脚本 前几节解决“如何用框架表达训练流程”，DeepSpeed 解决“如何把训练推进到多卡和更大模型规模”。\n第六章进一步给出了：\npretrain.py finetune.py pretrain.sh finetune.sh ds_config_zero2.json 这说明训练流程已经从“演示思路”切换到了“脚本化运行”。\npretrain.sh 的典型启动方式大致是这样：\n1 2 3 4 5 6 7 8 9 10 11 12 13 deepspeed pretrain.py \\ --config_name autodl-tmp/qwen-1.5b \\ --tokenizer_name autodl-tmp/qwen-1.5b \\ --train_files autodl-tmp/dataset/pretrain_data/xxx.jsonl \\ --per_device_train_batch_size 16 \\ --gradient_accumulation_steps 4 \\ --output_dir autodl-tmp/output/pretrain \\ --learning_rate 1e-4 \\ --num_train_epochs 1 \\ --block_size 2048 \\ --bf16 \\ --gradient_checkpointing \\ --deepspeed ./ds_config_zero2.json 5.1 DeepSpeed 在这一章里主要承担两件事 第一，让训练自然走向多卡。第二，通过 ZeRO 等策略降低显存压力。\nds_config_zero2.json 里最该看的关键词是 zero_optimization.stage = 2。这表示采用 ZeRO-2，主要优化优化器状态和梯度的存储开销。\n5.2 从 notebook 到脚本，增加的是工程外壳 比如在 pretrain.py 里，第六章补上了很多真实训练需要的东西：\nHfArgumentParser 解析命令行参数 checkpoint 恢复 主进程数据预处理 SwanLab 日志 shell 层统一管理训练超参 这时可以区分两类内容：\nnotebook 负责验证思路 脚本负责承接长时间训练 复习锚点：DeepSpeed 和 shell 脚本把训练从教学演示推进到可长期运行、可恢复、可记录的工程流程。\n六、第五步：进入 SFT 后，差异主要在数据构造 Pretrain 和 SFT 可以共享同样的 Trainer、DeepSpeed 和大部分训练框架，但它们的数据构造逻辑并不一样。\n原因是训练目标不同：Pretrain 学整个文本分布，SFT 学“给定指令后 assistant 应该怎样回答”。\n所以进入 SFT 之后，要先问：\n对话怎么拼 角色怎么标 哪些 token 参与 loss padding 和 mask 怎么处理 第六章这里沿用了 Qwen 风格的 chat template，骨架大致像这样：\n1 2 3 4 5 6 7 8 9 10 11 12 13 im_start = tokenizer(\u0026#34;\u0026lt;|im_start|\u0026gt;\u0026#34;).input_ids im_end = tokenizer(\u0026#34;\u0026lt;|im_end|\u0026gt;\u0026#34;).input_ids IGNORE_TOKEN_ID = tokenizer.pad_token_id nl_tokens = tokenizer(\u0026#34;\\n\u0026#34;).input_ids def preprocess(sources, tokenizer, max_len): input_ids, targets = [], [] ... return dict( input_ids=torch.tensor(input_ids), labels=torch.tensor(targets), attention_mask=input_ids.ne(tokenizer.pad_token_id), ) 6.1 这一节要盯住 labels 的构造方式 对 SFT 来说，不是所有 token 都应该参与 loss。\n设计上要分清：\nsystem 部分不参与拟合 user 部分不参与拟合 assistant 回复部分才是主要监督目标 这也是为什么 SFT 数据集看起来和 Pretrain 都是 input_ids + labels + attention_mask，但实际语义完全不同。\n如果用一句更压缩的话来说：\nPretrain 的 labels 是整段文本的镜像，SFT 的 labels 只对回答区域打开监督信号。\n6.2 chat template 是训练协议的一部分 在 SFT 中，\u0026lt;|im_start|\u0026gt;、\u0026lt;|im_end|\u0026gt;、角色标识、换行符这些 token 不是普通格式装饰，而是在定义：\n对话轮次边界 角色身份 system/user/assistant 的结构关系 所以，SFT 数据处理其实是在定义对话训练协议。\n6.3 attention_mask 和 padding 依然不能忽视 到这里之后，padding 会影响：\n哪些位置被模型看到 哪些位置不参与 loss batch 内不同长度样本如何对齐 很多实现里 IGNORE_TOKEN_ID 会取 -100，因为这是 PyTorch 交叉熵里常见的忽略标签值。第六章的脚本更偏教学示意，复习时重点记“标签遮蔽”这个动作。\n复习锚点：SFT 和 Pretrain 最大的不同，不在 Trainer，而在“谁参与监督”。\n七、第六步：资源不够时，就把全量微调切到 LoRA 第六章最后补上高效微调。它先介绍 Adapter、Prefix Tuning 等思路，再把重心落到 LoRA：用较少可训练参数完成任务适配。\nLoRA 的形式可以写成：\n$$ W = W_0 + BA $$其中 $W_0$ 是冻结的原始权重，$A$ 和 $B$ 是低秩增量矩阵。训练时原始权重保持冻结，主要更新这两个低秩矩阵。\n工程落点主要是 peft 的用法：\n1 2 3 4 5 6 7 8 9 10 11 from peft import get_peft_model, LoraConfig, TaskType peft_config = LoraConfig( task_type=TaskType.CAUSAL_LM, inference_mode=False, r=8, lora_alpha=32, lora_dropout=0.1, ) model = get_peft_model(model, peft_config) 7.1 LoRA 在这一章里负责的是“把可训练参数缩到更小” 它最适合的场景，是：\n模型参数很大 训练资源有限 任务更偏行为适配或领域适配 在训练链路上，LoRA 给 SFT 这类后训练阶段提供了更轻量的参数更新方式。\n7.2 peft 抽象了 LoRA 注入过程 对工程实践来说，我会记这几件事：\n目标层往往是 q_proj、v_proj 这类注意力投影层 LoraConfig 用来描述 LoRA 超参 get_peft_model 会把 LoRA 注入到原模型里 实战里要决定：\n微调目标是什么 LoRA 应该插到哪些层 rank、dropout、alpha 怎么取 图：LoRA 用低秩增量更新替代全量参数更新\n7.3 LoRA 的边界 LoRA 适合任务适配和低成本后训练；如果目标是让模型吸收大量新知识，它往往不是最理想路径。\n所以从训练链路上看，我更愿意这样理解：\nPretrain 更偏知识底座 SFT 更偏助手行为塑形 LoRA 更偏低成本适配 复习锚点：LoRA 是把后训练成本压下来的高效微调路线，不是替代预训练的知识注入方案。\n八、写在最后：第六章搭起的训练骨架 整章可以总结成下面这条主线：\n用 AutoConfig / AutoModel / AutoTokenizer 接入模型和 tokenizer 用 datasets.map 和 group_texts 把预训练数据整理成 CLM block 用 TrainingArguments + Trainer 承接训练骨架 用 DeepSpeed + ZeRO-2 把训练推向更真实的多卡脚本环境 在 SFT 阶段把重心切到 chat template + labels + attention_mask 在资源受限时用 LoRA + peft 替代全量微调 第六章搭起来的是一套基于主流框架的训练流程骨架：\nPretrain -\u0026gt; SFT -\u0026gt; LoRA\n这条链路里，每一层的职责也更清楚了：\n模型层负责承接结构和权重 数据层负责定义训练协议 Trainer/DeepSpeed 负责承接训练基础设施 LoRA 负责在后训练阶段压缩更新成本 6.4 的偏好对齐部分可以看成全景补充：训练链路后面还会走向奖励模型、RLHF、DPO 等阶段。但第六章落到可复用实践上的重点，仍是前面这条框架化训练主线。\n复习结论：第六章把大模型训练放进了主流工程栈里，而不是只停在模型结构图上。\n参考资料 Datawhale. Happy-LLM：第六章《大模型训练流程实践》 Datawhale Happy-LLM 仓库 Hugging Face Transformers Hugging Face PEFT ","date":"2026-04-21T00:00:00Z","image":"/p/happy-llm-transformers-training-practice/cover.svg","permalink":"/p/happy-llm-transformers-training-practice/","title":"用 Transformers 跑通 LLM 训练流程：预训练、SFT 和 LoRA"},{"content":"从 Transformer 模块继续往上看，LLM 可以先当成一个系统来理解：概率语言模型底座 + 分阶段训练 + 输入输出协议。\n这篇文章把两条线接起来：一条是训练目标、能力边界和对齐思路，另一条是 tokenizer、样本构造、loss mask、generate 等实现位置。复习时重点看：每个抽象能力最后落到哪种数据格式、训练信号或推理流程。\n一、先把 LLM 的整体框架理顺 理解“大语言模型”时，先不要停在“更大、更强”。更清楚的入口是三个问题：底层目标是什么，能力为什么会变化，训练为什么要分阶段。\n1.1 LLM 的本质仍然是语言模型 LLM 的底层目标可以先记成一句话：它仍然是在做 next-token prediction。\n如果写成概率形式，就是：\n$$ p(x) = \\prod_{t=1}^{T} p(x_t \\mid x_{1:t-1}) $$这意味着，无论模型后来表现得多像助手、推理器或知识库，底座训练仍是在学习“给定前文，下一个 token 最可能是什么”。这个目标会直接决定 Pretrain 的数据组织、SFT 的标签构造，以及推理阶段逐 token 生成的方式。\n1.2 LLM 的强，来自规模化系统共同作用 不要把 LLM 简化成“更大的 BERT”或“参数更多的 Transformer”。这样说太省事，也容易漏掉训练系统本身。\n模型能力的变化，背后往往是多种因素一起放大的结果：\n模型规模变大 数据规模变大 训练算力变大 训练策略和数据配比更成熟 所以，LLM 的能力提升不能只解释成“网络更深”。更准确的说法是：规模化训练让语言建模系统在某些规模阈值后表现出新的能力形态。\n1.3 区分“涌现、上下文学习、指令遵循、逐步推理” 这些词都和“模型变强”有关，但对应不同层面的能力：\n涌现能力 说的是：当模型、数据和训练规模跨过某个阈值之后，一些原来很弱甚至几乎看不见的能力会突然变得明显。 上下文学习（In-Context Learning） 说的是：模型不需要参数更新，只通过 prompt 和 few-shot 示例，就能在当前上下文里临时适配一个任务。 指令遵循 说的是：模型开始学会把输入理解成“用户要求我做什么”，并按要求组织回答。 逐步推理 说的是：模型在处理复杂问题时，能够通过中间步骤把问题拆开，而不是一次性拍出最终答案。 复习时不要把它们混成一句“模型更聪明了”。看到一个模型表现变好，要继续追问：是知识覆盖更充分、上下文利用更好、指令理解更稳，还是推理步骤更完整？\n1.4 多语言、长上下文、多模态是扩展能力，幻觉是系统风险 还有一个判断要尽早建立：能力扩展和可靠性不是同一件事。\n模型可以支持多语言、长上下文，甚至接入图像等多模态输入，但这些能力不等于可靠性。幻觉提醒我们：LLM 仍是概率生成系统，输出流畅并不等于事实正确。\n复习锚点：长上下文不等于稳定推理，助手风格不等于事实可靠，语言流畅性不等于知识正确性。\n1.5 Pretrain、SFT、RLHF/DPO 之所以要分开，是因为它们在解决不同问题 大模型训练要拆成阶段理解，因为每个阶段解决的问题不同。先看粗线条：\n图 1. 训练 LLM 的三个阶段，图片引自 Happy-LLM 原文\n阶段 核心目标 复习要点 Pretrain 学知识底座与文本分布 让模型先变成一个会建模语言分布的系统 SFT 把模型塑形成能回答问题的助手 重点是教会它输入输出格式、回答风格和对话模式 RLHF / DPO 让模型更符合人类偏好与安全边界 重点是“更有用、更安全、更符合偏好” “训练一个 LLM”不是单一动作。Pretrain、SFT 和偏好对齐各自改变不同东西：知识底座、回答行为、偏好边界。只有把阶段分开，后面看代码、数据格式和 loss 才不会混。\n从 ChatGPT 的训练流程看，对齐阶段还能拆成监督微调和偏好优化两层。SFT 和 RLHF/DPO 不能混成一步，因为前者主要学习“怎样回答”，后者主要学习“哪种回答更符合偏好和安全要求”。\n图 2. ChatGPT 三阶段训练示意，图片引自 Happy-LLM 原文\n二、再把最小训练闭环看清楚 前面解释“为什么这样训练”，接下来要看这些目标在代码和流程里落在哪里。最小训练闭环可以整理成：\ntokenizer -\u0026gt; 数据样本构造 -\u0026gt; Decoder-Only 模型 -\u0026gt; loss 计算 -\u0026gt; 训练循环 -\u0026gt; 生成\n只要这条链路打通，tokenizer、loss、mask、训练循环和生成就不再是散点概念。\n2.1 Tokenizer 是一整套输入协议 Tokenizer 不只是“把文本切成 token”的工具，它是一套输入协议。\n除了 BPE、ByteLevel、NFKC 这些技术选择，还要关注：\n特殊 token 如何定义 对话起止符号如何设计 角色边界如何标记 编码和解码是否能稳定保真 chat template 如何把多轮对话拼成单个序列 把 tokenizer 理解成协议后，很多问题会变得具体：模型如何知道当前是 user 还是 assistant，哪里应该开始回答，多轮历史怎样保持结构。这些都由特殊 token 和 chat template 规定。\n2.2 看 Decoder-Only 模型，不要只背模块名 看 LLaMA 风格模型结构时，不要只背模块名，要问每个模块解决什么问题：\n先看整体结构，再把下面这些设计放回主干：\n图 3. LLaMA2 结构图，图片引自 Happy-LLM 原文\nRMSNorm：让深层网络训练更稳定。 RoPE：把位置信息注入注意力计算，常用于相对位置建模。 GQA 和 repeat_kv：在注意力结构里节省 KV 缓存和推理显存。 SwiGLU 风格的 MLP：增强前馈网络的表达能力。 embedding 和 lm_head 权重共享：让输入词表表示和输出词表预测共用一套参数。 复习模型结构时，按“模块 -\u0026gt; 解决的问题 -\u0026gt; 对训练/推理的影响”来记。\n2.3 Pretrain 和 SFT 共享 CLM 底座，差别在监督边界 无论是预训练数据，还是 SFT 数据，最底层都还是在做自回归建模。也就是说，数据往往还是会被整理成类似这样的形式：\nX = input_ids[:-1] Y = input_ids[1:] 所以可以记住：Pretrain 和 SFT 共享同一个 CLM 底座。\n它们的差别主要在“哪些 token 参与监督”。预训练通常让整个序列参与语言建模；SFT 会把 system/user/history 作为上下文输入，但只让 assistant 的回答部分承担 loss。\nloss_mask 的作用就是把“只塑形回答行为”落到训练信号边界上。\n2.4 generate 解释了为什么生成是逐 token 进行的 生成时，模型会在当前上下文上做前向传播，只取最后一个位置的 logits，再根据 temperature、top_k 等采样策略决定下一个 token。新 token 会被拼回上下文，进入下一轮循环。\n这个过程解释了三个现象：\n为什么训练和推理虽然相关，但运行方式并不完全一样 为什么采样参数会显著影响模型输出风格 为什么大模型输出会一点点长出来 模型重复、越说越跑偏、不同采样参数导致结果差异很大，都和这个逐 token 生成循环有关。\n三、把概念和实现一一对应 复习 LLM 时，最有效的方法是把抽象概念和代码落点对应起来：训练目标对应数据样本，助手行为对应 loss mask，对话能力对应 chat template，长上下文对应位置编码和注意力开销。\n3.1 从 next-token prediction 到 X/Y shift next-token prediction 在代码里会直接变成 X/Y shift：\n输入看前面的 token 标签预测后一个 token 这就是最基本的 shift 操作。所谓“语言模型底座”，就是一种会直接决定数据组织方式和 loss 定义的训练协议。\n3.2 从“助手塑形”到 loss_mask SFT 的目标是“助手行为塑形”。落实到实现里，问题会收缩成一句话：\n哪些 token 应该参与监督，哪些 token 只是上下文条件？\nloss_mask 把 SFT 的监督边界写进了数据里：用户输入和历史对话提供条件，assistant 的回答部分提供监督信号。这个边界比“做监督微调”这个词本身更重要。\n3.3 从“多轮对话能力”到 special tokens 与 chat template 多轮对话能力也要落到数据格式上。只说它来自 SFT 和对齐还不够，还要看训练数据如何被组织成模型能理解的序列。\nspecial tokens 和 chat_template 会显式标记：\n一轮对话从哪里开始 user 和 assistant 的边界在哪里 多轮历史应该怎样拼接 哪一段是当前应该回答的部分 从这个角度看，对话能力是一套训练协议、数据格式和监督方式共同塑造出来的行为结果。\n3.4 从“长上下文与位置感”到 RoPE 长上下文也要落到结构和成本上。它不仅是“能放更多文本”，还牵涉位置编码、训练长度、推理缓存和注意力开销。\nRoPE 解决的是“位置信息如何进入注意力计算”。长上下文能力不是一句宣传语，背后有位置编码设计、上下文长度外推策略和推理时 KV cache 成本。\n如果把视角进一步缩到注意力层里，这张图就很适合用来理解 RoPE、GQA 和 repeat_kv 为什么总是一起出现：\n图 4. LLaMA2 Attention 结构图，图片引自 Happy-LLM 原文\n复习时要把“模型能力”和“结构选择”对应起来，而不是把二者当作独立话题。\n3.5 从“训练阶段”到“训练闭环” 训练阶段的拆分，最后也会落实成一条可执行的训练闭环。一个能跑起来的训练系统，至少要把很多基础环节连起来：\n学习率调度 warmup 梯度累积 AMP 混合精度 梯度裁剪 checkpoint 保存 训练不是只写一个 loss。模型目标、数据格式、mask 设计、优化器设置、训练循环、checkpoint 和生成行为共同构成闭环。缺少其中任意一块，对系统的理解都会变得片面。\n四、这篇先留下哪些结论 复习这篇时，我先保留下面几条。\n4.1 LLM 先是语言模型，然后才是助手 它的根始终在 next-token prediction。只有先接受这一点，后面关于对齐、助手风格、推理链、工具调用之类的能力，才有一个稳定的起点。\n4.2 LLM 的能力要拆开看，不能混成一句“它更聪明了” 涌现、上下文学习、指令遵循、逐步推理对应不同类型的能力变化。把这些概念分开，是理解模型行为边界的前提。\n4.3 三阶段训练对应的是三类不同问题 Pretrain 学的是语言和知识底座，SFT 学的是助手式回答行为，RLHF/DPO 学的是偏好与安全边界。它们对应不同目标。\n4.4 很多“能力”最后都会落成训练协议 对话能力会落到 special tokens 和 chat_template，SFT 会落到 loss_mask，语言建模目标会落到 X/Y shift，长上下文能力会落到 RoPE 与注意力成本。\n4.5 理解 LLM，要把概念、数据和代码三层接起来 只学概念容易空，只看代码容易碎。要同时问两件事：为什么这样设计，它在实现里长什么样。\n五、写在最后：怎样理解 LLM 这篇最后可以整理成一个框架：\nLLM = 概率语言模型底座 + 分阶段训练 + 一整套输入输出协议。\n以后再看一个模型、训练脚本或对齐方法时，可以用四个问题检查：\n它的底层目标是什么？ 它属于训练链条的哪一个阶段？ 它改变的是知识、行为、偏好，还是输入输出协议？ 它在实现里最终落到了什么地方？ 这套检查方式能把“大语言模型为什么这样训练”和“代码为什么这样写”连起来。继续学习 LoRA、QLoRA、DPO 或其他对齐训练时，也可以沿着这条线看：它改的是参数更新方式、监督边界、偏好目标，还是输入输出协议。\n参考资料 Datawhale. Happy-LLM：第四章《大语言模型》 Datawhale. Happy-LLM：第五章《动手搭建大模型》 Datawhale Happy-LLM 仓库 ","date":"2026-04-18T00:00:00Z","image":"/p/happy-llm-llm-to-training-loop/cover.webp","permalink":"/p/happy-llm-llm-to-training-loop/","title":"从大语言模型到训练闭环：我现在怎样理解 LLM"},{"content":"从 Transformer 的实现细节往上看，预训练语言模型的分化主要由三件事决定：目标函数、信息流方式、工程可扩展性。\n这篇文章主要回答三个问题：BERT 为什么适合理解任务，T5 为什么适合统一的 text-to-text 任务，GPT/LLaMA 为什么在大模型阶段更常见。复习时先抓住一个判断框架：模型能看见什么信息、被训练去预测什么、能否高效扩展。\n一、在比较模型之前，先把三个底层件讲清楚 1.1 Tokenizer 是文本到 token 的输入协议 无论 BERT、T5 还是 GPT，第一步都是先把文本稳定地映射为离散 token ID。这个过程通常包括：\n文本规范化 预切分 子词切分 映射到词表 ID 加入特殊 token Tokenizer 要在 词表规模、未登录词覆盖率、序列长度、训练与推理成本 之间做平衡。\n这也是为什么 BERT 会用 WordPiece，RoBERTa 更偏向 byte-level BPE，而 LLaMA 又会在 tokenizer 设计上继续优化编码效率。\n1.2 Embedding 的本质，是一个可学习的查表矩阵 Embedding 层本质上是一个可训练的查表矩阵：\n$$ E \\in \\mathbb{R}^{V \\times d} $$其中 $V$ 是词表大小，$d$ 是隐层维度。输入一个 token ID，本质上就是从矩阵里取出对应的一行向量。\n如果写成 one-hot 形式，它等价于：\n$$ e = x^\\top E $$也正因为如此，词表一旦变大，Embedding 参数会迅速膨胀，这正是 ALBERT 要做 Embedding 分解的直接背景。\n1.3 预训练目标，往往比“模型名字”更能决定能力边界 同样是 Transformer，不同预训练目标会强烈影响模型最终擅长什么：\nMLM：更适合理解和表征学习 span corruption：适合条件生成和统一任务接口 CLM：最贴近真实生成过程，训练目标与推理过程一致 所以比较预训练模型时，不要只记模型名字，要先看它的预训练目标。目标函数会决定模型更偏理解、条件生成，还是开放式续写。\n二、三条路线：分别解决三类问题 架构 代表模型 预训练目标 注意力可见性 更擅长的任务 Encoder-Only BERT / RoBERTa / ALBERT MLM（+ NSP / SOP） 双向 分类、匹配、检索、序列标注 Encoder-Decoder T5 span corruption / 去噪重建 Encoder 双向，Decoder 因果，并带 Cross-Attention 翻译、摘要、问答、条件生成 Decoder-Only GPT / LLaMA CLM 因果单向 开放生成、对话、代码、长文本续写 这张表给出一个复习判断：\n模型结构和“它想看见什么信息、又想预测什么目标”强耦合。\n三、BERT 路线：为什么它能统治预训练模型时代的 NLU BERT 把 预训练 + 微调 这条路推成了 NLP 的主范式：模型先在大规模语料上学习通用表征，再到具体任务数据上微调。\n图 1. BERT 架构图，图片引自 Happy-LLM 原文\n3.1 BERT 为什么天然适合理解任务 BERT 本质上是把 Transformer 的 Encoder 堆起来。\n这里要记两个特点：\n双向自注意力：每个 token 都能同时看到左边和右边上下文。 输出的是上下文化表示：它更适合作为文本编码器，而不是直接生成器。 这意味着 BERT 很擅长回答这类问题：\n这句话表达的是正面还是负面？ 这两个句子是不是语义相近？ 这个 token 在句子里应该被标成什么类别？ 这些都属于“先理解，再判别”的任务。\n3.2 MLM + NSP：BERT 当时怎么做预训练 BERT 最有代表性的预训练任务是 MLM + NSP。\nMLM（Masked Language Model） 的核心是“完形填空”：\n随机选取 15% token 做预测目标 其中 80% 替换成 \u0026lt;MASK\u0026gt; 10% 随机替换 10% 保持不变 这个设计很巧。它既让模型学习双向上下文，又尽量缓和了“预训练时看见 \u0026lt;MASK\u0026gt;，下游使用时看不见 \u0026lt;MASK\u0026gt;”的分布落差。\nNSP（Next Sentence Prediction） 则让模型学习句间关系。\n虽然这个任务后来被广泛质疑，但在 BERT 当时的设计里，它表达的是另一个训练诉求：模型不只学 token 级语义，也要学句级关系。\n3.3 BERT 之后，大家主要在优化什么 BERT 之后的改进，主要围绕两个瓶颈展开：训练是否充分，以及参数是否冗余。\nRoBERTa 的改进方向是“训练得更充分”：\n去掉 NSP，只保留 MLM 用动态 masking 替代静态 masking 训练更多 token、更大 batch、更长步数 使用更大的 BPE 词表 它传达出的信号非常明确：很多时候，问题出在训练规模还没打满。\nALBERT 的改进方向则是“把参数做轻”：\nEmbedding 分解：把 $V \\times H$ 变成 $V \\times E + E \\times H$ 跨层参数共享：多层重复计算，但共享同一层参数 用 SOP 替换 NSP，强化句序关系建模 ALBERT 的复习要点是：参数量减少，不等于计算量按比例下降。 它主要减少存储和参数冗余，不等于计算量也会按同样比例下降。\n3.4 BERT 路线的边界也很清楚 BERT 直到今天依然非常有用，尤其是在：\n标注数据比较充分的分类任务 高吞吐、低延迟的判别式场景 检索、排序、匹配、序列标注 但它的短板同样明显：它不是为原生生成设计的。\n如果任务目标是开放式生成，BERT 通常就不再是最自然的选择。\n四、T5 路线：把任务统一成 text-to-text 如果说 BERT 偏表征学习，GPT 偏自回归生成，那么 T5 的做法是：把所有任务都表述成 text-to-text。\n图 2. T5 架构图，图片引自 Happy-LLM 原文\n4.1 为什么 Encoder-Decoder 结构这么自然 T5 保留了完整的 Encoder 和 Decoder：\nEncoder 负责把输入“读懂” Decoder 负责在因果约束下逐步生成输出 Cross-Attention 负责让 Decoder 在每一步都能“按需查询” Encoder 的结果 这套结构非常适合翻译、摘要、生成式问答这类任务，因为它们天然就是：\n输入一段文本，输出另一段文本\n这和 Seq2Seq 任务的形式完全匹配。\n4.2 T5 真正想统一的是任务接口 T5 的基本想法是：所有 NLP 任务都写成文本到文本。\n例如：\n情感分类：classify: 这是一个很好的产品 -\u0026gt; positive 摘要：summarize: ... -\u0026gt; 摘要文本 翻译：translate English to German: ... -\u0026gt; 德文结果 这种设计把原本碎片化的任务接口统一成同一种输入输出形式：\n不需要为每个任务单独设计输出头 不需要为不同任务维护完全不同的输入输出形式 多任务训练和迁移更容易组织 复习 T5 时要记住：它不只是一个 Encoder-Decoder 模型，也是在标准化任务表达方式。\n4.3 span corruption 为什么比逐 token mask 更合理 T5 的预训练采用 span corruption：\n随机遮蔽连续文本片段 用哨兵 token（如 \u0026lt;extra_id_0\u0026gt;）占位 让 Decoder 重建被遮蔽的原始 span 这比逐 token mask 更贴近自然语言中的“片段恢复”，也更符合 Encoder-Decoder 的生成机制。\n4.4 T5 为什么没有成为今天 LLM 的绝对主流 T5 很适合条件生成，但在超大规模时代没有成为最主流基座，主要受工程链路影响：\n架构比 Decoder-Only 更复杂 推理链路更长 部署和时延成本通常更高 可以这样记：T5 的优势是任务表达统一；Decoder-Only 的优势是生成链路更简洁、更容易扩展。\n五、GPT 路线：为什么大模型时代最终收敛到 Decoder-Only 如果说 BERT 代表预训练模型时代的理解路线，GPT 则代表大模型时代的自回归生成路线。\n图 3. GPT 架构图，图片引自 Happy-LLM 原文\n5.1 GPT 的设计到底解决了什么问题 GPT 采用 Decoder-Only + CLM 的组合，本质上是在做自回归建模：\n$$ p(x) = \\prod_{t=1}^{T} p(x_t \\mid x_{1:t-1}) $$这意味着模型在第 $t$ 个位置只能看到历史 token，不能看到未来 token，所以必须使用 causal mask。\n这个设计带来几个直接结果：\n任务高度匹配：天然就是为生成服务 训练和推理一致：训练时预测下一个 token，推理时也生成下一个 token 结构更简洁：不需要 Encoder，也不需要 Cross-Attention 更容易做大规模扩展：训练、部署、推理链路都更直接 复习 GPT 时要抓住一句话：Decoder-Only + CLM 把训练目标、推理方式和工程扩展统一到了“预测下一个 token”上。\n5.2 一个很容易混淆的点：CLM 训练并不慢在“逐 token” 很多人第一次接触 GPT 会以为：既然是自回归，那训练是不是也要一个 token 一个 token 地慢慢跑？\n训练阶段：常用 teacher forcing，可以一次前向并行计算所有位置的损失 推理阶段：需要逐 token 自回归生成，因为每一步都依赖上一步生成的新 token 所以 causal mask 限制的是“信息可见性”，不等于训练时完全失去并行性。\n5.3 GPT-1 到 GPT-3 验证了什么 从 GPT-1 到 GPT-3，最重要的是一个路线被连续验证了：\n模型规模继续增大 预训练数据继续增大 训练工程继续增强 zero-shot / few-shot 开始明显展现价值 这条路线验证了一件事：当 CLM 配上足够大的模型和数据时，模型不只会生成文本，也会在很多理解任务上表现出泛化能力。\n这也是为什么后来 LLM 逐步从“预训练 + 微调”范式，走向了“预训练 + 指令跟随 + 上下文学习”的新范式。\n六、LLaMA：Decoder-Only 路线走向工程成熟的代表 LLaMA 没有推翻 GPT 的基本方向，而是在 Decoder-Only + CLM 路线上做了更工程化、更容易复用的实现。\n图 4. LLaMA 架构图，图片引自 Happy-LLM 原文\n6.1 LLaMA 继承了什么 LLaMA 继续采用 Decoder-Only 主干，因此保留了 GPT 路线的几个优点：\n单栈解码器，结构清晰 因果建模，目标直接 易于扩展到更大参数量与更长上下文 6.2 LLaMA 又优化了什么 按这部分学习材料，LLaMA 系列的代表性优化包括：\n使用 RoPE 注入位置信息，增强相对位置建模能力 在更大版本上引入 GQA 等注意力优化 使用更高质量、更大规模的预训练语料 持续优化 tokenizer 与训练配方 这些改动单看都不是“换范式”，但合在一起，会让同样的 Decoder-Only 路线更稳，也更适合真实应用。\n6.3 为什么今天很多 LLM 都接近 LLaMA 路线 进入大模型阶段，基座模型需要同时满足这些条件：\n能不能稳定训练到很大 能不能高效推理 能不能方便做指令微调和对齐 能不能作为通用基座支撑聊天、代码、Agent、工具调用 在这几个指标上，Decoder-Only 路线很占便宜。\n所以从历史上看，LLaMA 的流行，更像是 GPT 路线进一步工程化后的结果。\n七、GLM：一条很有启发性的“融合路线” GLM 这条路线特别有意思，因为它尝试把 MLM 的双向理解能力 和 CLM 的自回归生成能力 合在一起。\n它的几个设计点包括：\nblank infilling 式预训练 特殊 attention mask 二维位置编码 在被遮蔽片段内部按自回归顺序生成 这条路线想做的事很直接：既要理解，也要生成。\n从今天回头看，GLM 没有成为超大规模基座的主流，但它并不“失败”。\n更准确地说，GLM 留下了一个路线判断：\n在预训练模型时代，融合式目标很有吸引力；但到超大规模 LLM 阶段，工程可扩展性和训练稳定性会把很多选择重新拉回 CLM 主线。\n这也是为什么后续 ChatGLM 系列在大方向上逐渐回归更经典的 CLM 路线。\n八、这部分内容的三个复习结论 8.1 架构和目标函数是强绑定的 BERT 的强项来自双向表征学习 T5 的强项来自 Encoder-Decoder 的条件生成 GPT/LLaMA 的强项来自 CLM 的一致性和扩展性 8.2 预训练模型时代和大模型时代，最优解不一定相同 在 PLM 时代，BERT 这样的 Encoder-Only 模型完全可以统治 NLU。\n但到了 LLM 时代，随着模型和数据规模继续放大，Decoder-Only 开始在综合能力上占据主导。\n8.3 实现细节背后都有取舍 Tokenizer、Embedding、mask、位置编码、参数共享，这些看似实现细节的设计，都对应具体的建模取舍。复习时要能回答：\n它为什么要这样看信息？ 它为什么要这样定义训练目标？ 它为什么在那个时代有效，又为什么在另一个时代失势？ 九、写在最后：模型路线的认知框架 如果前面对 Transformer 的学习主要回答“模型是怎么工作的”，那么这一篇要回答的是：为什么不同阶段会偏向不同架构。\nBERT 的优势来自双向理解，T5 的优势来自 text-to-text 统一接口，GPT/LLaMA 的优势来自简单一致的生成目标和更直接的规模化路径。GLM 则提醒我：融合式目标很有启发，但路线能不能走远，还要看工程上能不能稳定放大。\n继续往下看时，可以重点追问一个问题： 为什么一旦进入 LLM 时代，Decoder-Only + CLM 会从“其中一条路线”变成“几乎默认路线”？\n有了目标函数、信息流和扩展性这三个维度，这个问题就可以具体分析，而不只是凭感觉判断。\n参考资料 Datawhale. Happy-LLM：第三章《预训练语言模型》 Jacob Devlin et al. BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding Yinhan Liu et al. RoBERTa: A Robustly Optimized BERT Pretraining Approach Zhenzhong Lan et al. ALBERT: A Lite BERT for Self-supervised Learning of Language Representations Colin Raffel et al. Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer Alec Radford et al. Improving Language Understanding by Generative Pre-Training Tom B. Brown et al. Language Models are Few-Shot Learners Zhengxiao Du et al. GLM: General Language Model Pretraining with Autoregressive Blank Infilling ","date":"2026-04-14T00:00:00Z","image":"/p/happy-llm-pretrained-language-models/cover.svg","permalink":"/p/happy-llm-pretrained-language-models/","title":"预训练语言模型复习：BERT、T5、GPT/LLaMA 各走了哪条路"},{"content":"引子：从一次性问答到可维护知识库 2026 年 4 月的一天，Andrej Karpathy 在 GitHub Gist 上发布了一篇名为 LLM Wiki 的文档。它讨论的不是一次性使用 ChatGPT，而是如何让 LLM 持续维护个人知识库。\n这篇笔记记录两件事：Karpathy 的 LLM Wiki 模式是什么，以及我怎么把它落到自己的 LLM 学习系统里。复习时主要看三层结构和三种操作。\n一、LLM Wiki：从 RAG 到持久知识编译 1.1 它想解决什么 常见的 LLM 文档问答模式是 RAG（Retrieval-Augmented Generation）：上传文档 -\u0026gt; 检索相关片段 -\u0026gt; 生成回答。它能解决单次问答，但有个问题：知识不会自动沉淀。昨天问答中的归纳、矛盾和交叉引用，不会自然变成今天可复用的知识结构。\nKarpathy 提出的替代方案是让 LLM 增量构建并维护一个持久 Wiki：\n当你加入新资料时，LLM 会阅读它、提取关键信息、整合进现有 Wiki，并更新实体页面、修订主题总结、标记新数据与旧论断的矛盾、强化或质疑正在演进的综合分析。\n区别在于：Wiki 是一个持续维护的知识层。它不只是临时检索结果，而是把摘要、概念页、实体页、矛盾标记和综合分析长期保存下来。\n1.2 三层架构 1 2 3 4 5 6 7 ┌──────────────────────────────────────┐ │ Schema（AGENTS.md / CLAUDE.md） │ ← 你和 LLM 共同维护的行为规范 ├──────────────────────────────────────┤ │ Wiki（markdown 文件目录） │ ← LLM 写、你读的知识层 ├──────────────────────────────────────┤ │ Raw Sources（原始文档） │ ← 不可变的来源真值 └──────────────────────────────────────┘ Raw Sources：原始材料层，保存论文、文章、代码笔记等来源。只读，不让 LLM 修改。 Wiki：知识加工层，保存摘要页、实体页、概念页、比较分析等 Markdown 页面。LLM 主要维护这一层。 Schema：维护规则层，用来规定 LLM 如何 ingest、query、lint，以及页面格式、引用规范和日志规则。 1.3 三种操作 操作 描述 产出 Ingest 投入新资料，LLM 阅读、摘要、更新现有页面 一次 ingest 可能触及 10–15 个 Wiki 页面 Query 向 Wiki 提问，LLM 综合相关页面生成带引用的回答 好的回答可以回写为 Wiki 新页面 Lint LLM 对 Wiki 做健康检查——矛盾、孤立页、缺失引用 维护 Wiki 的长期质量 1.4 为什么这行得通 维护知识库的难点不在于阅读或思考——在于记账。更新交叉引用、保持摘要最新、标记新旧数据冲突、维护一致性。人类放弃 Wiki 是因为维护成本增长得比价值更快。LLM 不会厌倦，不会忘记更新引用，能一次性修改 15 个文件。\n复习锚点：Raw Sources 保真，Wiki 负责综合，Schema 约束维护行为。这个模式的价值不在“自动写笔记”，而在降低长期维护成本。\n这个模式让我想到 Vannevar Bush 1945 年的 Memex 构想：一个带有文档间关联路径的个人知识存储。过去的难点是“谁来维护链接和摘要”；LLM 让这部分维护成本降了下来。\n二、实践：我的 LLM Wiki 搭建 我在自己的知识库中按这个模式搭了一个最小结构：\n2.1 目录结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Wiki/ ├── AGENTS.md # Schema：工作流规范 ├── index.md # 内容索引：按类别编目所有页面 ├── log.md # 时间线：append-only 的操作日志 ├── raw/ # Raw Sources 层 │ ├── index.md # 来源注册表 │ └── inbox/ # 新材料暂存区 │ └── llm-zero-to-one/ # LLM 学习专题 └── pages/ # Wiki 层 ├── sources/ # 每份来源的摘要页 ├── concepts/ # 跨来源的概念归纳页 ├── entities/ # 实体页（人物/工具/模型） ├── queries/ # 问答沉淀页 └── synthesis/ # 综合分析页 2.2 流程实录 以收录 Happy-LLM 第一章为例，一次 ingest 大概是这样：\n将文章 Markdown 放入 raw/inbox/llm-zero-to-one/ 在 raw/index.md 登记为 Status = selected LLM 阅读全文，生成 pages/sources/happy-llm-chapter1-nlp-basics.md LLM 判断该来源属于 \u0026ldquo;LLM 从零学习\u0026rdquo; 概念域，更新 pages/concepts/llm-zero-to-one-learning-path.md 更新 index.md 的概念表条目 在 log.md 追加操作记录 一份来源会触及多个页面。随着来源增多，概念页会不断补充证据、链接和对比关系，这就是 Wiki 模式区别于普通摘抄的地方。\n2.3 我学到的一点 实践之后，我最想留下的是这句话：\n先搭可持续维护的知识系统，再扩充学科内容，可以少掉很多碎片化学习的损耗。\n这和传统“先学东西再整理笔记”的路径不同。学习系统先就位后，每一份新材料都会进入已有页面、索引和日志，而不是散落成临时笔记。\n三、这个模式具体解决什么 这个知识管理模式带来几个直接好处：\n学习有据可查：每次 ingest 和 query 都留有 log 记录，可以回溯学习轨迹。 碎片能慢慢成系统：零散笔记会进入概念页和综合页，不再只是一堆临时摘抄。 交叉引用不用全靠手动维护：不同知识点可以自动链接起来。 学习路径更可见：通过 Obsidian 的图谱视图，可以看到哪些知识节点已经建立，哪些还是空白。 四、下一步 按照 Happy-LLM 的课程路线，接下来的学习计划包括：\n预训练语言模型：比较 Encoder-only (BERT)、Encoder-Decoder (T5)、Decoder-only (GPT) 三种范式 大语言模型专题：LLM 的定义、涌现能力、训练策略 动手搭建：基于 PyTorch 实现 LLaMA2，从 Tokenizer 到预训练的完整链路 训练实践：预训练 → SFT → LoRA/QLoRA 高效微调 应用层：RAG 检索增强、Agent 智能体 每个阶段的学习材料都会通过 Wiki 的 ingest 流程进入知识网络。这篇博文本身，也算是一次从 Wiki 到 Blog 的输出。\n参考资料 Karpathy, A. LLM Wiki. GitHub Gist, 2026. Datawhale. Happy-LLM：从零开始构建大模型. GitHub, 2025. Bush, V. As We May Think. The Atlantic, 1945. ","date":"2026-04-12T00:00:00Z","image":"/p/llm-wiki-learning/cover.png","permalink":"/p/llm-wiki-learning/","title":"从 Karpathy 的 LLM Wiki 到我的知识系统"},{"content":"进入大语言模型之前，我先补两块东西：文本如何变成模型能处理的向量，以及 Transformer 如何用注意力机制建模上下文。这两件事没理顺，后面看 tokenizer、embedding、mask、训练目标和生成过程时很容易断线。\n这篇文章分成两部分：先梳理文本表示从统计方法到上下文化表示的演化，再整理手写 Transformer 时最容易混淆的九个实现问题。复习时重点看三条线：张量形状怎样变化、mask 怎样限制信息流、Encoder/Decoder 各自承担什么任务。\n一、温故知新：文本表示是如何进化到大模型时代的？ 在接触大模型结构之前，先回答一个底层问题：文本怎样被表示成模型可以计算的对象？\n1.1 NLP 范式的演进 NLP 的技术路线可以粗略分成三段。每一段都在回答同一个问题：如何把语言现象变成可计算、可学习的形式。\n规则方法（手写模板）：早期需要语言学专家人工定义无数规则。 统计方法（概率模型）：利用马尔可夫模型、隐马尔可夫模型等，让模型学习词串在语料库中出现的概率分布。 深度学习方法（端到端）：采用神经网络构建端到端的系统，无需繁杂的特征工程。 1.2 文本表示的“进化链” 大模型能处理长文本和上下文，前提是文本表示已经从稀疏统计特征走到稠密、上下文化向量。复习时我按这条链看：\n方法 核心思想 特点与局限 VSM (词袋模型) 将文本表示为高维稀疏向量，基于词频统计。 忽略了词语的先后顺序，也完全丢失了语义相似性信息。 N-gram 考察连续 n 个词的共现概率。 能捕捉一定的局部词序，但面临严重的维度爆炸和数据稀疏问题。 Word2Vec 用稠密、低维的向量表示词语，语义相近的词距离更近。 静态表示（Static Embedding），每个词具有固定的向量表征，无法解决“一词多义”问题。 ELMo 基于双向 LSTM，根据上下文动态生成词向量。 实现了动态表示，但受限于 RNN 架构，长距离依赖的建模能力仍然有限。 RNN/LSTM 按时间步递归处理序列，不利于大规模并行，也很难稳定抓住超长距离依赖。Transformer 的自注意力机制改成一次性比较序列中所有 token 之间的关系，所以更适合并行训练和长上下文建模。\n二、直面硬核：Transformer 学习过程中的 9 个迷思与解答 学习 Transformer 时，真正容易卡住的不是公式，而是公式落到代码以后：每个张量在哪一层、是什么形状、表示什么语义。下面九个问题都围绕一件事：把结构设计和实现细节对上。\n复习时可以把这些问题当成检查表：能回答清楚，就说明注意力、mask、残差、位置编码和编解码器分工已经基本接上。\nQ1：关于多头注意力参数的组织方式 为什么通常使用三个“组合特征矩阵”（比如 $W_Q, W_K, W_V$）而不是给每个注意力头分配单独的矩阵？\n在实现时，代码经常直接使用 nn.Linear(d_model, 3 * d_model) 或等价组合。这是因为 $X \\cdot W_{combined}$ 在数学上完全等价于先分别对每个头进行特征映射然后再将结果拼接起来。使用组合矩阵能够在 GPU 上将所有的计算打包成一次矩阵乘法（matmul），极大提升了计算并行度与效率。\nQ2：Causal Mask（因果掩码）的切片逻辑 代码中经常看到 scores = scores + mask[:, :, :seqlen, :seqlen]，这样做的意义是什么？\n因果掩码用来防止 Decoder 在自回归生成时“偷看未来”。通常这个 mask 张量会被构造成一个上三角全为 $-\\infty$、下三角（含对角线）全为 $0$ 的矩阵。 把它加到注意力分数 scores 后再做 softmax，未来位置的权重就会变成 $0$。最后的切片 :seqlen, :seqlen 是为了从预分配的大掩码里，取出当前序列长度真正需要的那一块。\nQ3：残差连接到底发生在什么位置 经常看见 Attention 模块里面有 self.wo 和 resid_dropout，它是直接在 Attention 里计算残差吗？\n这是一个常见误解。Attention 模块内部通常只做注意力计算、输出投影 self.wo 和 dropout，并不直接把输入 x 加回来。残差连接一般发生在外层的 EncoderLayer 或 DecoderLayer 中，以 x + sublayer_output 或 x + dropout(sublayer_output) 的形式完成。\nQ4：Encoder 与 Decoder 到底差在哪 作为整体，它们的分工到底有什么不同？\nEncoder 负责建模源序列内部的上下文。它是一个“通读全文”的结构，通常不需要因果掩码。 Decoder 在生成时必须遵守时间顺序。它先用带掩码的自注意力层（Masked Self-Attention）看自己已经生成的历史，再用交叉注意力层（Cross-Attention）读取 Encoder 的输出，最后通过前馈网络（FFN）得到用于预测下一个词的隐状态。 Q5：Self-Attention 🆚 Cross-Attention 自注意力和交叉注意力的结构区别在哪里？\n属性 Self-Attention (自注意力) Cross-Attention (交叉注意力) Q的来源 当前序列内部 目标序列（Decoder 端） K/V的来源 当前序列内部 源序列（Encoder 端） 解决的问题 “我这句话里的词之间，谁和谁有什么联系？” “我在生成当前目标词时，应该把注意力放在上一句（源句）的哪几个词上？” Q6：为什么 Decoder 必须要设计 Cross-Attention 只使用自回归生成不行吗？为什么还要多加一层交叉注意力？\n如果只靠 Masked Self-Attention，Decoder 只能看到目标侧已经生成的历史 token，无法直接利用源序列信息。Cross-Attention 让 Decoder 在每个生成位置都用当前目标侧状态作为 Query，去 Encoder 输出中检索相关的 Key/Value。这个机制相当于软对齐：不需要人工指定源词和目标词的对应关系，模型会通过注意力权重自己选择源端信息。\nQ7：位置编码（Positional Encoding）的直觉理解 Transformer 摒弃了 RNN 的时序结构，那它是怎么解决词语顺序问题的呢？\n注意力机制本身只计算 token 之间的相似度，不天然包含顺序信息。如果不加入位置编码，模型很难区分“猫追狗”和“狗追猫”这类词相同但顺序不同的句子。\n位置编码的作用是把位置信息放进 token 表示里。将位置编码向量加到词嵌入后，模型在计算注意力时既能比较“token 内容是什么”，也能利用“token 在哪个位置”。Sin/Cos 绝对位置编码使用不同频率的三角函数表示位置；三角函数有平移关系，所以它也能帮助模型推断相对距离。\nQ8：register_buffer 在 PyTorch 实现中的意义 有些模型参数会使用 register_buffer，这是为什么？\nregister_buffer 用于注册那些随模型保存（会被序列化进入 state_dict）并且会随模型调用 to(device) 自动迁移到 GPU，但不参与梯度更新（不可训练） 的状态张量。比如上面提到的、预先计算好的三角位置编码缓存以及因果掩码矩阵，这就是典型的使用场景。\nQ9：view 操作的作用场景 在多头注意力实现中，为什么最后经常要调用 view？\n多头注意力需要把隐藏层维度拆成多个头。例如把 [batch, seqlen, dim] 改成 [batch, seqlen, n_heads, head_dim]，其中 dim = n_heads * head_dim。view 的作用是重新解释张量形状，不改变元素总数，也不改变底层数据。复习时要记住：view 解决的是形状组织问题，不是新增参数或复制语义。\n三、接下来还想继续补上的问题 复习这篇时，我会把问题压成三句：\n文本表示的演化，是从稀疏词频走向上下文化向量。 Transformer 用注意力计算 token 间关系，再用 mask 和位置编码约束信息流。 手写实现时，最该盯住张量形状、残差所在层级、Encoder/Decoder 的信息来源。 接下来继续往下学时，重点就会自然转向三条路线：BERT 的 Encoder-Only、T5 的 Encoder-Decoder、GPT/LLaMA 的 Decoder-Only。\n","date":"2026-04-12T00:00:00Z","permalink":"/p/happy-llm-phase1-nlp-transformer/","title":"从 NLP 基础到 Transformer：我先补的几块地基"},{"content":"欢迎 这是我的第一篇博客文章！这个博客基于 Hugo 静态站点生成器和 Stack 主题搭建，托管在 GitHub Pages 上。\n为什么要写博客 记录学习：把学习过程中的笔记和心得沉淀下来 分享经验：将踩过的坑和解决方案分享给社区 倒逼输出：写作是最好的学习方式 写作计划 接下来我打算记录以下方向的内容：\n深度学习：Transformer、Mamba、视觉模型等 计算机视觉：目标检测、车道线检测、图像分割 工程实践：PyTorch 实战、Linux 环境配置、Git 工作流 竞赛复盘：挑战杯、创新创业、算法竞赛等 技术栈 1 2 3 4 5 Static Site Generator : Hugo (Extended) Theme : Stack v4 Hosting : GitHub Pages CI/CD : GitHub Actions Writing : Markdown 第一篇先写到这里。后面会慢慢把学习笔记和项目复盘搬上来。\n","date":"2026-04-12T00:00:00Z","permalink":"/p/hello-world/","title":"你好，世界！"}]