Update 大模型应用.md

新增 Text2SQL 一节(附录 A34),补充幂等键的生成与保存、熔断的失败统计、置信度校准、推理模型的结构化输出、长上下文 Embedding 的切片等内容;调整高频标记。
This commit is contained in:
CyC2018
2026-11-04 22:39:58 +08:00
parent c79f2f416b
commit 04e6cd5f11
+153 -17
View File
@@ -13,7 +13,7 @@
* [幻觉](#幻觉)
* [推理模型](#推理模型)
* [二、提示词与上下文](#二提示词与上下文)
* [提示词](#提示词)
* [提示词](#提示词) 🔥
* [结构化输出](#结构化输出)
* [上下文工程](#上下文工程)
* [三、检索增强生成](#三检索增强生成) 🔥🔥
@@ -31,13 +31,14 @@
* [长上下文与 Agentic RAG](#长上下文与-agentic-rag)
* [GraphRAG](#graphrag)
* [多模态 RAG](#多模态-rag)
* [Text2SQL](#text2sql)
* [四、Agent](#四agent)
* [Agent 是什么](#agent-是什么) 🔥🔥
* [工具调用](#工具调用)
* [Agent 循环](#agent-循环)
* [Agent 与工作流](#agent-与工作流)
* [意图识别与路由](#意图识别与路由)
* [规划](#规划) 🔥
* [规划](#规划)
* [记忆](#记忆) 🔥
* [多轮对话](#多轮对话)
* [MCP](#mcp) 🔥
@@ -88,6 +89,7 @@
* [A31 注意力分数的缩放](#a31-注意力分数的缩放)
* [A32 语义缓存的相似度](#a32-语义缓存的相似度)
* [A33 模型生成 SQL 的权限限制](#a33-模型生成-sql-的权限限制)
* [A34 Text2SQL 的口径与语义层](#a34-text2sql-的口径与语义层)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@@ -288,7 +290,7 @@ GQA 减少的是缓存和读取的数据量,注意力的计算量基本不变
几个参数同时设置时,顺序也有影响:vLLM 和 Transformers 先除以温度,再做 Top-k、Top-p;llama.cpp 默认最后才应用温度。温度不改变排序,但会改变 Top-p 留下几个 Token,例如上面的分布在温度 0.5、Top-p 0.8 时,先做温度只留下 2 个(0.75 + 0.15),后做温度留下 3 个。
请求中不传采样参数时,生效的往往是模型自带的 generation_config.json,例如 vLLM 默认读取它,Qwen2.5-7B-Instruct 中设的是温度 0.7、Top-p 0.8、Top-k 20。温度设为 0 时,vLLM 按贪心解码处理;Transformers 则要设置 do_sample=False。
请求中不传采样参数时,生效的往往是模型自带的 generation_config.json,例如 vLLM 默认读取它,Qwen2.5-7B-Instruct 中设的是温度 0.7、Top-p 0.8、Top-k 20,以及压低重复内容的 repetition_penalty 1.05。温度设为 0 时,vLLM 按贪心解码处理;Transformers 则要设置 do_sample=False。
抽取字段、生成代码、调用工具等需要稳定结果的场景使用较低的温度,写作类场景可以适当调高。
@@ -365,6 +367,8 @@ DPO 的损失只取决于两个回答的概率之差,需要注意的是:
如果 4 个回答全对或全错,优势都是 0,这道题对训练没有贡献,所以题目的难度要适中。数学、代码这类能自动判断对错的任务可以直接用规则打分,不需要奖励模型。DeepSeekMath 中的 GRPO 仍使用训练出的奖励模型,DeepSeek-R1 对推理任务改用规则打分,原因是神经网络的奖励模型在大规模强化学习中容易被模型钻空子(Reward Hacking),对通用的对话数据仍使用奖励模型。
规则打分同样会被钻空子。例如以测试通过为奖励训练写代码的模型时,模型可能修改测试文件,或者在断言执行之前调用 sys.exit(0),让测试进程以 0 退出、看起来全部通过。所以测试和打分代码要放在模型改不到的地方,并按测试的实际结果而不是进程的退出码判分。
GRPO 把 KL 散度直接加在损失中,概率之比的限制与 PPO 相同。PPO 把每个 Token 的 KL 惩罚加在奖励中,强化学习最大化的是累计奖励,回答越长累计的惩罚越多,DeepSeek-R1 的作者认为这可能抑制回答变长;GRPO 的 KL 项不进入优势的计算。R1 的训练中还每 400 步把参考模型换成最新的策略模型,让模型在长时间训练中能继续探索。
代价是每道题要生成一组回答,DeepSeekMath 中每题采样 64 个,DeepSeek-R1 中每题 16 个,生成的开销很大。
@@ -405,7 +409,7 @@ RAG、微调和提示词解决的是不同的问题:
通常先尝试提示词,再考虑 RAG,最后才考虑微调。微调需要准备训练数据,基座模型升级后要重新训练,而且写入参数的知识难以更新,也无法给出来源。
用微调灌输新知识的效果也不好。有实验表明,SFT 数据中包含模型预训练时不知道的事实时,这些样本学得明显更慢,而一旦被学会,模型编造答案的倾向随之增加。在这项闭卷问答的实验中,负面影响主要出现在训练后期,表现为过拟合,按验证集提前停止训练、去掉模型不知道的样本,或者把它们的答案改为“不知道”,都能减轻。
用微调灌输新知识的效果也不好。有实验表明(全参数微调),SFT 数据中包含模型预训练时不知道的事实时,这些样本学得明显更慢,而一旦被学会,模型编造答案的倾向随之增加。在这项闭卷问答的实验中,负面影响主要出现在训练后期,表现为过拟合,按验证集提前停止训练、去掉模型不知道的样本,或者把它们的答案改为“不知道”,都能减轻。
LIMA 只用 1000 条精选的样本微调 65B 的模型就得到了很好的对话效果,作者据此提出假说:知识和能力几乎都在预训练中学到,对齐主要教会模型回答时用什么格式。
@@ -543,7 +547,7 @@ FP8 有两种编码:E4M3(4 位指数、3 位尾数)最大 448,E5M2(5
- 每行(每个输出通道)一个系数:好一些,但这一行的最大值正好是这个离群值,同一行的其它权重仍然全部变成 0;
- 每组一个系数:离群值只影响它所在的组,另一组的权重得以保留。实际常用 128 个一组,每组多存一个 FP16 的缩放系数,平均每个权重多 0.125 位,7B 模型约多 0.12 GB。
把 Qwen2.5-7B 28 层中的全部线性层按不同的方式量化,在一段约 6600 个 Token 的中文技术文本上测量困惑度(越低越好):
把 Qwen2.5-7B 28 层中的全部线性层按不同的方式量化,在一段约 6600 个 Token 的中文技术文本上测量困惑度(越低越好)。困惑度(Perplexity)是模型给文本中每个 Token 的概率取负对数、求平均后再取指数,可以理解为模型平均在多少个候选之间犹豫;分词方式不同的模型之间不能直接比较:
<div align="center"> <img src="pics/quant-ppl.png" width="700px"/> </div><br>
@@ -655,7 +659,7 @@ Agent 的幻觉除了回答错误,还会出现在工具调用的过程中。
## 二、提示词与上下文
### 提示词
### 提示词 🔥
对话接口的输入是一组带角色的消息:系统(system)消息设定模型的身份和规则,用户(user)消息是具体的请求,助手(assistant)消息是模型之前的回答,工具(tool)消息是工具的执行结果。
@@ -686,6 +690,8 @@ Agent 的幻觉除了回答错误,还会出现在工具调用的过程中。
提示词和代码一样需要版本管理。提示词、模型版本、工具定义和采样参数共同决定了输出,最好作为一个整体编号发布,线上记录每次请求用的是哪个版本,新版本先灰度给一小部分用户,与旧版本对比线上指标,出问题时整体回滚;比较时把模型固定到具体的版本,一次只改一处,否则分不清变化来自提示词还是模型。
灰度按用户分流而不是按请求,否则同一段多轮对话中前后两轮用的版本不同,效果分不开。
### 结构化输出
程序需要解析模型的输出时,要求模型按照 JSON Schema 返回结果。JSON Schema 是用 JSON 描述数据格式的规范,写明有哪些字段、各自的类型、哪些必填以及取值范围,例如 {"type": "object", "properties": {"temp": {"type": "integer"}}, "required": ["temp"]} 要求输出一个带整数字段 temp 的对象。只在提示词中要求输出 JSON 是不够的,模型仍可能多说一句解释、漏掉字段或者少写一个括号。
@@ -704,6 +710,8 @@ Agent 的幻觉除了回答错误,还会出现在工具调用的过程中。
不按 Schema 约束顺序的 JSON 模式也可能出现这个问题,例如 Tam 等(2024)用 gpt-3.5-turbo-0125 的实验中,JSON 模式的输出全都把 answer 写在 reason 前面,没有先写推理步骤,拼接单词末尾字母(Last Letter)任务的平均准确率只有 1.78%;这项研究的提示词设计存在争议,不能据此认定 JSON 模式必然降低准确率(arXiv 2408.02442)。
把解释字段放在前面,针对的是不带思考的模型。推理模型在输出之前已经思考过,OpenAI 的文档认为不必再让它逐步推理;Claude 的文档还把 JSON 中的 reasoning、thinking 这类字段列为拒答的常见原因,需要说明时改为要求简短的理由或依据(以官方最新文档为准)。
#### 2. 输出无法解析时
不支持约束解码时,模型的输出可能无法解析。最常见的是 JSON 前后多了内容,例如包在 ```` ```json ```` 代码块里;其次是输出达到 max_tokens 被截断(接口返回的 finish_reason 为 length),以及语法合法但不符合 Schema,例如枚举值写成了同义词、金额写成“59 元”这样的字符串。
@@ -906,6 +914,8 @@ Word、HTML、Markdown 文件本身记录了标题样式、表格和列表,按
- Embedding 模型有最大长度,bge-small-zh-v1.5 最多 512 个 Token,超出的部分直接被截断,800 字的中文约 680 个 Token;
- 放进提示词的内容越少,成本越低,引用的来源也越精确。
换成最多 8192 个 Token 的模型虽然不会截断,第一条仍然成立。例如用 jina-embeddings-v2-small 在 LongEmbed 的英文长文本检索任务上,把文本截到 8192 个 Token 后,按 512 个 Token 切片比整段编码的 nDCG@10 平均相对提高约 24%(arXiv 2409.04701)。
但切片也不能太小。例如“工作满 1 年不满 10 年的”和“每年 5 天”被切到两段,单独召回哪一段都无法回答问题。切片太大,向量不够准确,无关的内容还会占用上下文。
#### 2. 常用做法
@@ -1084,6 +1094,8 @@ embedding 由 text 经 Embedding 模型计算得到,bge-small-zh-v1.5 输出 5
文档修改后,按文档 ID 和内容的哈希值找出变化的文档,删除它的全部旧切片再写入新切片,不需要重建整个索引;只追加新切片会让旧内容继续被召回。只是权限或版本状态变化时,更新切片上的元数据即可,不必重新计算向量。文档的原文和元数据一般以关系数据库为准,向量库按切片 ID 幂等写入,失败时重试,并定期对账两边的数据。要写入向量库的切片可以在修改文档的同一个事务中记一条同步记录,由后台读取后写入向量库,做法同[本地消息表](分布式.md#本地消息表),文档写成功、向量写失败时有记录可以重试。
库存、价格这类变化频繁、要求准确的数据以业务系统为准,回答时用工具查询,知识库主要保存说明类、变化较慢的内容。有有效期的内容,例如促销规则,在元数据中记下生效和失效时间,检索时按问题涉及的时间过滤,问的是现在的规则就用当前时间。
哈希只能发现被修改的文档,整篇源文件被删除时,要靠文档系统的删除事件,或者定期拿源系统的全部文档 ID 与库中比对,删掉多出来的切片。
删旧切片和写新切片不是一次完成的,期间查询可能缺少这篇文档或新旧内容并存;在意这一点时,先写入带新版本号的切片,再把元数据中的当前版本切过去,最后删除旧切片。另外,Milvus 默认的一致性级别是 Bounded,默认容忍最近约 5 秒内写入的数据不可见;查询必须看到刚才的写入时指定 Strong,只需要读到同一个客户端自己的写入时可以用 Session,代价是查询要等待数据可见。
@@ -1242,7 +1254,7 @@ RAG 需要分别评测检索和生成,这样才能定位问题出在哪一步
#### 3. 评测集
指标要在一批固定的问题上计算。问题优先从真实的用户问题中抽样,不够时让模型根据文档生成,再人工改写措辞,因为模型生成的问题往往照抄原文的词语,会高估向量检索的效果。除了常见问题,还要包含资料中没有答案的问题、需要多个切片的问题和新旧版本冲突的问题。没有答案的问题,标准答案标为“资料中没有”,回答不知道算对,编出答案算错;它和有答案的问题被误拒要一起统计,两者的取舍与[相似度阈值](#4-相似度阈值)相同。
指标要在一批固定的问题上计算。问题优先从真实的用户问题中抽样,不够时让模型根据文档生成,再人工改写措辞,因为模型生成的问题往往照抄原文的词语,会让检索的评测结果偏高,尤其容易让按词匹配的 BM25 受益,向量检索和混合检索也会受影响。除了常见问题,还要包含资料中没有答案的问题、需要多个切片的问题和新旧版本冲突的问题。没有答案的问题,标准答案标为“资料中没有”,回答不知道算对,编出答案算错;它和有答案的问题被误拒要一起统计,两者的取舍与[相似度阈值](#4-相似度阈值)相同。
每个问题标注标准答案和回答所需的原文片段,而不是切片 ID,这样改变切片方式之后标注仍然可用,切片一节的实验就是这样判断是否找到的。几十到几百个问题就可以比较不同的方案(差距只有几道题时要结合逐题的结果判断,比较方法见 Agent 的[评测集与线上指标](#4-评测集与线上指标)),每次调整解析、切片、模型或参数,以及知识库中新增大量文档之后,都重新评测;线上发现的错误回答持续补充进来,避免评测集与真实的问题分布脱节。
@@ -1308,6 +1320,51 @@ GraphRAG 在建索引时让模型从每个切片中抽取实体和关系,组
检索到图片之后,生成用的是多模态模型时,可以把图片直接放进上下文让它读图回答;只能用文本模型时,就只能使用图片的文字描述,模型并没有看到图片本身。
### Text2SQL
“10 月的销售额是多少”这类问题要对表中的数据做筛选和汇总,答案不在任何一段文字里,把表的内容切片检索也算不出来。自然语言转 SQL(Text-to-SQL,常写作 Text2SQL)指的是让模型把问题写成 SQL,由程序执行,再把结果交给模型组织成回答,或者直接展示成表格。
主要的难点有两个:
- 表结构对应:问题中的“品类”“支付用户”对应哪张表的哪一列、表之间怎样关联,表很多时还要先选出相关的表,只把它们的结构放进提示词;
- 业务口径:“销售额”算不算已退款的订单、“客单价”除以订单数还是用户数,表结构里看不出来。
BIRD 基准中,GPT-4 只看表结构时执行准确率为 34.88%,再给出口径和取值含义这类外部知识后为 54.89%(arXiv 2305.03111)。
例如订单表 orders 有金额 amount 和状态 status,明细表 order_items 有品类 category、单价和数量,字段都带注释,用 6 个 2026 年 10 月的统计问题测试 Qwen2.5-7B-Instruct(见[附录 A34](#a34-text2sql-的口径与语义层))。
只给表结构时答对 1 个:3 个问题的口径错了,例如销售额把已退款、已取消和待支付的订单也加了进去,得到 1290 而不是 620;另外 2 个用了表中不存在的列,执行报错。在提示词中写明 6 个指标的口径后,6 个都答对。
口径写在提示词里,换一个应用、一张报表就可能出现另一种算法。语义层(Semantic Layer)把指标的计算方法和可以分组的维度统一定义好,模型只输出要查哪个指标、按什么分组、查哪个月,由代码或引擎生成 SQL,例如 dbt 的 MetricFlow、Cube。
上面的例子改成这种方式后,模型对“10 月每个品类的销售额”输出:
```json
{"metric": "销售额", "group_by": ["品类"], "month": "2026-10"}
```
代码按“销售额”的定义生成 SQL,按品类分组时用明细的单价乘数量,只算已支付的订单:
```sql
SELECT i.category, SUM(i.price * i.quantity)
FROM orders o JOIN order_items i ON i.order_id = o.id
WHERE o.status = 'PAID' AND o.created_at >= '2026-10-01' AND o.created_at < '2026-11-01'
GROUP BY i.category;
```
6 个问题也都答对。其中一次模型输出的 JSON 没有给“销售额”加引号,带着解析错误重试一次后改对,用[约束解码](#1-约束解码)可以避免这类问题。
表之间的关联也交给语义层。订单关联到明细后,订单上的金额随明细重复出现,例如订单 1001 有 3 条明细,直接对 orders.amount 求和时它的 300 元被算了 3 次,10 月的销售额变成 1220。MetricFlow 按事先声明的实体关系选择关联路径,并限制这种一对多的扇出(fan-out)关联。
语义层的代价是要先建模、持续维护,而且只能查已经定义的指标和维度。定义之外的问题要先补充定义,临时的查询也可以退回由模型或分析师直接写 SQL。
其它常用的做法:
- 表结构带上字段说明和取值的含义,例如 status 的每个取值代表什么;
- 把验证过的“问题—SQL”存下来,按问题的相似度检索几条放进提示词作为示例;
- SQL 执行出错时把错误信息回传给模型修改,结果为空或明显不合理时向用户确认口径;
- 用只能读取指定视图的账号执行,并限制执行时间和返回的行数,见[限制模型能造成的后果](#2-限制模型能造成的后果)。
## 四、Agent
### Agent 是什么 🔥🔥
@@ -1439,6 +1496,8 @@ String run(String question) {
什么时候结束由模型判断,但模型可能一直不停,或者反复调用同一个工具,所以代码中必须限制最大步数,见下文的可靠性与评测。
代码中的 tools 就是工具的注册表:以工具名为键,保存发给模型的定义和执行它的函数,请求时把其中的定义放进 tools 参数,模型返回调用后按名字找到函数执行,新增一个工具只要注册一项。
execute 不抛出异常,而是把错误写成文字作为工具结果返回,模型看到后可以修正参数、换一个工具或者向用户说明。下面是简化的写法,省略了参数校验和临时性错误的重试,实际应用中超时、限流这类错误先由代码重试,仍然失败或者需要模型处理的错误才作为工具结果返回,见[容错](#1-容错):
```java
@@ -1528,7 +1587,7 @@ for (CompletableFuture<Message> f : results) {
阈值要在验证集上扫一遍再定:阈值越高,自动路由的比例越低,准确率一般越高。例如上面 Embedding 相似度方法的置信度阈值从 0.5 提到 0.7,自动路由从 70% 降到 48%,准确率从 90.5% 升到 96.6%,其余转入兜底流程。按兜底流程能承接的量和可以接受的错误率选一个点,分错代价高的类别(例如会进入退款流程的)可以单独设更高的阈值。
阈值的效果还取决于置信度是否可靠:7B 模型分错的 3 条中有 2 条的置信度高于 0.93,阈值几乎拦不住。大模型的输出概率往往过于自信,可以改用多次采样看结论是否一致,或者在类别中加入“不确定”,让模型自己选择。
阈值的效果还取决于置信度是否可靠:7B 模型分错的 3 条中有 2 条的置信度高于 0.93,阈值几乎拦不住。大模型的输出概率往往过于自信,部分模型在 RLHF 等后训练之后校准明显变差,可以在验证集上做温度缩放,把过于集中的概率摊平,或者多次采样看结论是否一致;让模型自己报置信度,或者在类别中加一个“不确定”让模型自己选,效果都难以保证,要在验证集上检验。
路由的准确率要靠评测集来衡量和守住:
@@ -1538,7 +1597,7 @@ for (CompletableFuture<Message> f : results) {
- 线上转人工、用户改口重说的会话说明路由可能错了,定期抽查后加入评测集;
- 修改类别定义、提示词或者模型之后,在同一个评测集上回归,确认没有让其它类别变差。
### 规划 🔥
### 规划
规划要解决的是一个任务分几步、按什么顺序做、做错了怎么办。常见的几种方式的区别在于:推理是否与行动交替,是否先给出完整的计划,是否同时探索多条思路,失败后是否总结经验再试。
@@ -1720,7 +1779,7 @@ Agent 运行到一半时进程可能重启,用户也可能隔天才回来。
进程重启后,按会话 ID 读出消息列表和任务状态,根据最后一条消息决定从哪里继续:
- 最后一条是用户消息,或者上一轮的工具调用都有了结果:重新调用一次模型即可;
- 有工具调用还没有对应的结果(一次回复中有多个调用时,可能只保存了其中一部分的结果):说明工具执行到一半中断了。查询类的工具直接重新执行;退款、改地址这类修改数据的工具,以工具调用的 ID 作为幂等键,先查询这次操作是否已经生效,再决定是否执行,避免重复退款。
- 有工具调用还没有对应的结果(一次回复中有多个调用时,可能只保存了其中一部分的结果):说明工具执行到一半中断了。查询类的工具直接重新执行;退款、改地址这类修改数据的工具,以工具调用的 ID 作为幂等键(这条带工具调用的回复已经保存,ID 不会变),先查询这次操作是否已经生效,再决定是否执行,避免重复退款。
用户隔天回来时,会话的短期记忆可能已经过期,前一天的完整对话也不应该再放进上下文。做法是会话结束时生成一份摘要,连同未完成的待办一起保存;新会话开始时放入长期记忆和上次会话的摘要。例如用户前一天说“税号我待会儿发你”,第二天只发来一串税号,Agent 根据上次的待办就知道这是订单 1024 的发票,不需要用户重新解释。
@@ -1873,7 +1932,7 @@ MCP 也有代价。全量加载时,每个服务端的工具定义都会进入
Anthropic 还介绍过一种做法:把 MCP 工具生成为代码文件,Agent 按需读取工具的定义、编写代码调用,中间结果留在执行环境中,不经过模型,例子中的 Token 消耗从 15 万降到 2000。
另外,第三方服务端的工具描述和返回结果可能包含恶意指令,应该像安装软件一样只使用可信的服务端。MCP 规范仍在快速演进,本节对照 2026-07-28 版,细节以官方最新版本为准。
另外,第三方服务端的工具描述、服务端说明(instructions,客户端可能把它放进系统提示词)和返回结果都可能包含恶意指令,应该像安装软件一样只使用可信的服务端。MCP 规范仍在快速演进,本节对照 2026-07-28 版,细节以官方最新版本为准。
### Skills 🔥
@@ -2201,6 +2260,8 @@ checkpoint 什么时候写入由 durability 参数决定:默认的 async 在
Spring AI 是 Spring 生态中的 AI 集成层,作用类似 Spring Data 之于数据库:用统一的 ChatClient 访问不同厂商的模型,用 @Tool 把 Java 方法注册为工具,用 Advisor(类似拦截器)加入对话记忆、RAG 等功能,用 entity() 把模型输出转换为 Java 对象,同时提供向量库的统一接口和 MCP 的客户端、服务端支持。
本节按 Spring AI 2.0 编写,它要求 Spring Boot 4;Spring Boot 3 的项目使用 Spring AI 1.1。两者在工具调用和记忆上有差别:1.1 中工具调用的循环由 ChatModel 内部执行,没有默认的调用次数上限,记忆 Advisor 的默认顺序也不同,以官方升级说明为准。
```java
class OrderTools {
@Tool(description = "按订单号查询订单状态和运单号。用户询问订单、发货情况时使用")
@@ -2297,11 +2358,15 @@ tool 退款申请已提交,单号 R88
有副作用的操作要额外处理:
- 幂等:修改数据的工具超时,操作可能已经执行,只是响应没有返回;模型重复调用、从 checkpoint 恢复时节点重新执行,也会让同一个操作执行两次。所以这类工具要用幂等键去重,重试时沿用同一个键,例如同一个订单只能有一条进行中的退款,见[幂等](分布式.md#九幂等)。幂等键由程序在调用前生成并随任务保存,不让模型生成;参数变了就是新的操作,换新的键并重新确认。框架的自动重试(例如 LangGraph 节点的 RetryPolicy)也是重跑整个节点,所以自动重试的节点应当没有副作用,或者副作用是幂等的;
- 幂等:修改数据的工具用幂等键去重,重试时沿用同一个键,例如同一个订单只能有一条进行中的退款,见[幂等](分布式.md#九幂等);
- 人工确认:转账、删除数据、对外发送信息等操作由代码校验权限,执行前暂停,把操作内容展示给人确认,实现见 [checkpoint 与人工介入](#2-checkpoint-与人工介入),待确认的操作和参数保存在服务端,确认请求只带操作 ID 和决定,同一个操作只能被确认执行一次(例如原子地把状态从待确认改为执行中),避免重复提交或确认的内容被替换;
- 补偿:多步操作中后面的步骤失败、前面的需要撤销时,执行对应的补偿操作,例如取消已提交的退款申请,思路与 [Saga](分布式.md#saga) 相同。只补偿已确认成功的步骤,下游既不支持幂等、也查询不到结果的步骤标为结果未知,转人工或对账,不自动重试,也不自动补偿;
- 隔离:执行代码的工具放在沙箱中运行,并记录审计日志。
同一个操作可能执行两次:工具超时时操作可能已经执行,只是响应没有返回;模型会重复调用,从 checkpoint 恢复时节点会重新执行,框架的自动重试(例如 LangGraph 节点的 RetryPolicy)也是重跑整个节点。所以自动重试的节点应当没有副作用,或者副作用是幂等的。
幂等键由程序生成,不让模型生成,要标识一次业务操作,例如退款申请号;参数变了就是新的操作,换新的键并重新确认。键要在执行工具之前写入存储,否则工具执行之后、checkpoint 保存之前进程崩溃,恢复时会生成新的键,去重就失效了,LangGraph 默认的 async 写入就有这个窗口,见 [checkpoint 与人工介入](#2-checkpoint-与人工介入)。
沙箱一般要限制可写的路径(工作目录和必要的临时目录等)、网络(默认拒绝或按域名白名单放行)、CPU 和内存,并由调用方计时、超时后终止。密钥不放进沙箱,可以由沙箱外的代理在转发请求时加上,沙箱里的代码即使被诱导也读不到密钥。
常见的隔离机制有三种:共享宿主机内核的容器,配合 seccomp 限制可用的系统调用;gVisor 在用户态实现 Linux 的内核接口,容器中的程序不直接调用宿主机内核;Firecracker 这类轻量虚拟机有独立的内核,借助硬件虚拟化隔离。隔离效果取决于配置,开销取决于实现和工作负载,按要防范的风险选择。
@@ -2361,7 +2426,7 @@ checks:
- 给出参考:把工具的返回结果或者标准答案交给裁判,要求以它为准;
- 把评分标准拆成具体的、可以逐项判断的问题,例如“是否给出了运单号”“位置是否与物流接口返回的一致”,而不是笼统地打 1~10 分;
- 裁判使用与被评测的模型不同系列的模型,并允许输出“无法判断”;
- 用一小批人工标注的样本检查裁判与人的一致率,一致率足够高之后再大规模使用,之后定期抽查。
- 用一小批人工标注的样本检查裁判与人的一致率,一致率足够高之后再大规模使用,之后定期抽查,失败和正常的样本要分别统计,因为线上的会话大多是正常的,总是判通过的裁判一致率也会很高。
#### 4. 评测集与线上指标
@@ -2380,7 +2445,7 @@ checks:
选模型时可以先参考公开的基准(例如测多学科选择题的 MMLU、测修复真实 GitHub issue 的 SWE-bench),但还要用自己的业务评测集验证:题目可能已经进了训练数据,按 GSM8K 新出一套同类的题时,部分模型的准确率下降了多达 8 个百分点(GSM1k);题目本身也可能有错,估计 MMLU 约有 6.49% 的题有错(MMLU-Redux)。
选型时先按硬条件筛选:数据能否交给外部服务、许可是否允许商用,上下文和输出长度是否够用,是否支持需要的工具调用和结构化输出;再在业务评测集上比较剩下几个模型的效果、延迟和每次任务的费用。
选型时先按硬条件筛选:数据能否交给外部服务、许可是否允许商用(同一系列不同尺寸的许可也可能不同,例如 Qwen2.5 的 7B 用 Apache 2.0,3B 只允许研究和评估,以模型发布页的许可为准),上下文和输出长度是否够用,是否支持需要的工具调用和结构化输出;再在业务评测集上比较剩下几个模型的效果、延迟和每次任务的费用。
服务商的模型名还可能改指新的模型,旧模型也会下线,例如 DeepSeek 的 deepseek-chat 先后对应过 11 个底层模型,阿里云百炼的正式版模型在启动下线后 3 个月停止服务。服务商提供固定版本的模型名(例如带日期的快照)时,生产环境尽量用它,换模型前在同一批用例上逐条对比(以各服务商的最新文档为准)。
@@ -2416,7 +2481,7 @@ data: [DONE]
```
- 每条事件由若干行“字段: 值”组成,以空行结束,data 是事件的内容(一条事件有多行 data 时按换行拼接),id 是事件的编号,event 是事件的类型(省略时为 message);
- 以冒号开头的行是注释,客户端会忽略,常用作心跳,防止中间的代理因为连接长时间空闲而断开;
- 以冒号开头的行是注释,客户端会忽略,常用作心跳,防止中间的代理因为连接长时间空闲而断开,间隔要小于代理的读超时,例如 Nginx 的 proxy_read_timeout 默认 60 秒,计的是两次读之间的间隔;
- 浏览器的 EventSource 断线后会自动重连,并在请求首部 Last-Event-ID 中带上最后收到的事件编号,服务端可以据此从断点继续发送。EventSource 只能发 GET 请求、不能带请求体,对话接口用 POST 发送问题时,前端通常用 fetch 读取流,这时重连和 Last-Event-ID 都要自己实现;
- OpenAI 兼容的接口用 data: [DONE] 表示生成结束。
@@ -2560,7 +2625,8 @@ Token 消耗增长过快时,先按步骤和工具汇总每一次调用的输
推理优化的手段按解决的问题分为几类:
- 每一步都要读一遍权重和 KV Cache:用[量化](#量化)、[GQA、MLA](#自回归生成与-kv-cache) 缩小要读的数据,用 [FlashAttention](#2-计算量与-flashattention) 减少注意力计算中的显存读写;
- 每一步都要读一遍权重和 KV Cache:用[量化](#量化)、[GQA、MLA](#自回归生成与-kv-cache) 缩小要读的数据;
- 注意力的中间矩阵要读写显存:用 [FlashAttention](#2-计算量与-flashattention) 分块计算,不把中间矩阵写回显存,收益主要在预填充和长上下文,解码时读写的大部分仍是 KV Cache;
- 显卡同时处理的请求太少:批处理、连续批处理、PagedAttention;
- 重复计算相同的开头:[前缀缓存](#5-前缀缓存);
- 只能逐个 Token 解码:投机解码(Speculative Decoding),小模型先连续猜几个 Token,大模型读一遍权重就能同时验证:设大、小模型给某个猜测的概率分别为 p、q,它以 min(1, p / q) 的概率被接受;第一个被拒绝的位置改从 max(0, p − q) 归一化后的分布重新采样,丢弃后面的猜测。这样输出的分布与大模型单独生成相同,适合并发不高的场景。
@@ -2614,7 +2680,7 @@ vLLM 的调度器维护运行和等待两个队列。每一步处理的 Token
128 56 GB 70.2 GB
```
vLLM 默认使用 92% 的显存(--gpu-memory-utilization)。在 24 GB 的显卡上,扣除权重后剩不到 7.9 GB,再扣除激活值等运行时开销,同时只能处理十几个满 8K 的请求;80 GB 的显卡上可以处理 100 多个。KV Cache 不够时,vLLM 抢占最晚加入的请求:释放它的 KV Cache,放回等待队列的最前面,之后重新做预填充,延迟明显增加,监控中的抢占次数(vllm:num_preemptions)持续增长。想用同样的显卡服务更多请求,可以减小最大上下文长度、使用更多显卡,或者对权重和 KV Cache 进行[量化](#量化)。
vLLM 默认使用 92% 的显存(--gpu-memory-utilization)。在 24 GB 的显卡上,扣除权重后剩不到 7.9 GB,再扣除激活值等运行时开销,同时只能处理十几个满 8K 的请求;80 GB 的显卡上可以处理 100 多个。KV Cache 不够时,vLLM 抢占最晚加入的请求:释放它的 KV Cache,放回等待队列的最前面,之后重新做预填充,延迟明显增加,监控中的抢占次数(Prometheus 中的 vllm:num_preemptions_total)持续增长。想用同样的显卡服务更多请求,可以减小最大上下文长度、使用更多显卡,或者对权重和 KV Cache 进行[量化](#量化)。
模型一张卡放不下时用张量并行,把每层的权重矩阵切到多张卡上,每层的注意力和前馈网络算完后各要在卡间做一次 all-reduce 汇总结果,所以一般只在 NVLink 高速互联的一台机器内使用,跨机器时再按层切分(流水线并行)。
@@ -2708,6 +2774,8 @@ OpenAI 的官方 SDK 默认也是这样处理的:对 408、409、429 和 5xx
熔断用于避免每个请求都先等主模型超时再切换。主模型在一段时间内的失败率超过阈值时,熔断器打开,之后的请求直接走备用模型,过一段时间再放行少量请求试探主模型是否恢复。限流、降级和熔断的原理见[系统设计基础](系统设计基础.md#限流降级与熔断)。
统计失败率时要区分错误的类型:短暂的速率限制(429)按 retry-after 等待后重试,一般不计入模型失败,否则突发的限流就可能让熔断器打开,持续限流、重试耗尽时再决定是否切换;流式响应可能在返回 200 之后才在流中报错,要按流中的错误事件判断;判断慢调用要看首 Token 的时间和之后每个 Token 的间隔,回答长本来就慢,按总耗时会误判。
#### 4. 流式输出中途断开
处理方式取决于断开时已经输出了什么:
@@ -3925,6 +3993,70 @@ OK SELECT /*+ MAX_EXECUTION_TIME(100) */ SLEEP(1) FROM v_orders LIMIT 1 -> ((
前三条和最后三条由 bi 执行:bi 读不到原表的手机号,也不能删除;它可以把自己的会话设为只读,再改回读写。中间两条由 root 执行,说明以 WITH 开头的语句也可以删除数据,只检查语句开头的关键字拦不住。最后一条的 SLEEP 返回 1,表示执行到 100 毫秒时被中断。
### A34 Text2SQL 的口径与语义层
Qwen2.5-7B-Instruct,float16 加载到 MPS,贪心解码(Transformers 5.20.2、PyTorch 2.8.0);SQL 在 MySQL 8.0.44 上执行,Apple M1 Pro。表结构和数据:
```sql
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT COMMENT '用户 ID',
amount DECIMAL(10,2) COMMENT '订单实付金额',
status VARCHAR(16) COMMENT '订单状态:UNPAID 待支付,PAID 已支付,REFUNDED 已退款,CANCELLED 已取消',
created_at DATETIME COMMENT '下单时间'
);
CREATE TABLE order_items (
id INT PRIMARY KEY,
order_id INT COMMENT '订单 ID',
category VARCHAR(16) COMMENT '商品品类',
price DECIMAL(10,2) COMMENT '单价',
quantity INT COMMENT '数量'
);
-- orders:1001 用户 1 300 元 PAID 10-03、1002 用户 2 120 元 PAID 10-08、1003 用户 1 200 元 PAID 10-12、
-- 1004 用户 3 80 元 UNPAID 10-20、1005 用户 4 500 元 REFUNDED 10-25、1006 用户 2 90 元 CANCELLED 10-28、
-- 1007 用户 5 260 元 PAID 09-30、1008 用户 3 150 元 PAID 11-01
-- order_items:1001 有服装 100 × 1、服装 50 × 2、食品 100 × 1;1002 食品 60 × 2;1003 数码 200 × 1;
-- 1004 食品 40 × 2;1005 数码 500 × 1;1006 服装 90 × 1;1007 服装 130 × 2;1008 食品 150 × 1
```
三种做法的系统提示词:A 为“根据下面的表结构,把用户的问题写成一条 MySQL 查询,只输出 SQL”加上面的建表语句;B 在 A 之后加上指标口径;C 只列出可用的指标和维度,要求输出 {"metric", "group_by", "month"} 格式的 JSON,由代码按指标的定义生成 SQL,JSON 解析失败时带着错误重试一次。B 的指标口径:
```text
- 销售额:已支付订单(status = 'PAID')的 amount 之和,已退款、已取消、待支付的订单不计入;
- 按品类统计销售额时,用 order_items 的 price × quantity 求和(同样只算已支付订单),不要把 orders.amount 关联到明细后求和;
- 支付用户数:有已支付订单的不同用户数;
- 客单价:销售额 ÷ 支付用户数;
- 退款率:已退款订单数 ÷(已支付订单数 + 已退款订单数);
- 销量:已支付订单中 order_items.quantity 之和。
```
执行结果与标准答案比较(比率写成百分数也算对),A 的输出(节选):
```text
标准答案:销售额 620,品类销售额 数码 200、服装 200、食品 220,支付用户 2,客单价 310,退款率 0.25,销量 7
[错] 销售额 SELECT SUM(o.amount) FROM orders o WHERE YEAR(o.created_at) = 2026 AND MONTH(o.created_at) = 10 -> 1290
[错] 品类销售额 SELECT o.category, SUM(oi.price * oi.quantity) FROM order_items oi JOIN orders o ... GROUP BY o.category -> ERR 1054
[对] 支付用户数 SELECT COUNT(DISTINCT user_id) FROM orders WHERE status = 'PAID' AND ... -> 2
[错] 客单价 SELECT AVG(amount) FROM orders WHERE YEAR(created_at) = 2026 AND MONTH(created_at) = 10 -> 215
[错] 退款率 SELECT COUNT(*) / SUM(CASE WHEN status = 'UNPAID' THEN 1 ELSE 0 END) FROM orders WHERE ... AND status = 'REFUNDED' -> NULL
[错] 销量 SELECT COUNT(*) FROM order_items WHERE YEAR(created_at) = 2026 AND MONTH(created_at) = 10 -> ERR 1054
A 只给表结构:1/6
B 表结构加指标口径:6/6
```
C 的输出(节选):
```text
[对] 销售额 第 1 次输出 {"metric": 销售额, "group_by": [], "month": "2026-10"},解析失败:Expecting value: line 1 column 12 (char 11)
重试后 {"metric": "销售额", "group_by": [], "month": "2026-10"} -> 620
[对] 品类销售额 {"metric": "销售额", "group_by": ["品类"], "month": "2026-10"} -> 数码 200、服装 200、食品 220
[对] 客单价 {"metric": "客单价", "group_by": [], "month": "2026-10"}
SELECT SUM(o.amount) / COUNT(DISTINCT o.user_id) FROM orders o WHERE o.status = 'PAID' AND ... -> 310
C 语义层:6/6
```
把 orders.amount 关联到明细后求和,10 月已支付订单的结果为 1220,按品类为服装 600、食品 420、数码 200。
## 参考资料
- [Attention Is All You Need](https://arxiv.org/abs/1706.03762)
@@ -3955,6 +4087,7 @@ OK SELECT /*+ MAX_EXECUTION_TIME(100) */ SLEEP(1) FROM v_orders LIMIT 1 -> ((
- [Chain-of-Thought Prompting Elicits Reasoning in Large Language Models](https://arxiv.org/abs/2201.11903)
- [Zhao T Z, et al. Calibrate Before Use: Improving Few-Shot Performance of Language Models. arXiv:2102.09690](https://arxiv.org/abs/2102.09690)
- [OpenAI: Reasoning best practices](https://developers.openai.com/api/docs/guides/reasoning-best-practices)
- [Claude Docs: Refusals and fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback)
- [Efficient Guided Generation for Large Language Models](https://arxiv.org/abs/2307.09702)
- [Tam Z R, et al. Let Me Speak Freely? A Study on the Impact of Format Restrictions on Performance of Large Language Models. arXiv:2408.02442](https://arxiv.org/abs/2408.02442)
- [Farquhar S, et al. Detecting hallucinations in large language models using semantic entropy. Nature, 2024](https://www.nature.com/articles/s41586-024-07421-0)
@@ -3975,6 +4108,7 @@ OK SELECT /*+ MAX_EXECUTION_TIME(100) */ SLEEP(1) FROM v_orders LIMIT 1 -> ((
- [Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks](https://arxiv.org/abs/1908.10084)
- [Qu Y, et al. RocketQA: An Optimized Training Approach to Dense Passage Retrieval for Open-Domain Question Answering. arXiv:2010.08191](https://arxiv.org/abs/2010.08191)
- [Moreira G, et al. NV-Retriever: Improving text embedding models with effective hard-negative mining. arXiv:2407.15831](https://arxiv.org/abs/2407.15831)
- [Günther M, et al. Late Chunking: Contextual Chunk Embeddings Using Long-Context Embedding Models. arXiv:2409.04701](https://arxiv.org/abs/2409.04701)
- [Precise Zero-Shot Dense Retrieval without Relevance Labels](https://arxiv.org/abs/2212.10496)
- [Weller O, et al. When do Generative Query and Document Expansions Fail? arXiv:2309.08541](https://arxiv.org/abs/2309.08541)
- [RAGAS: Automated Evaluation of Retrieval Augmented Generation](https://arxiv.org/abs/2309.15217)
@@ -4174,3 +4308,5 @@ OK SELECT /*+ MAX_EXECUTION_TIME(100) */ SLEEP(1) FROM v_orders LIMIT 1 -> ((
- [vLLM: Speculative Decoding](https://docs.vllm.ai/en/latest/features/speculative_decoding/)
- [vLLM 源码:v1/core/sched/scheduler.py](https://github.com/vllm-project/vllm/blob/main/vllm/v1/core/sched/scheduler.py)
- [Ollama FAQ](https://docs.ollama.com/faq)
- [Li J, et al. Can LLM Already Serve as A Database Interface? A BIg Bench for Large-Scale Database Grounded Text-to-SQLs. arXiv:2305.03111](https://arxiv.org/abs/2305.03111)
- [dbt: Joins(MetricFlow)](https://docs.getdbt.com/docs/build/join-logic)