当你的 LLM 应用跑到生产环境,账单开始以肉眼可见速度增长时,你才会意识到一个残酷的事实:你花钱买的大部分 token,都是在让模型“说废话”。我们优化了 Prompt、加了缓存、换了更便宜的模型,却很少追问一个更底层的问题——模型生成的内容本身,是否携带了过多的语义冗余?
最近引起我注意的一个研究方向,是Semantic Thermodynamics(语义热力学)。它的核心主张听起来甚至有点颠覆:通过引入Narrative Constraints(叙事约束),可以让 LLM 在生成层面主动减少 token 消耗。论文和实验材料里最吸引眼球的一个数字是79% token reduction,也就是说,在特定任务上,用同样的模型,只因为改变了生成时的约束方式,就省下了近八成的 token。
这篇文章不打算把语义热力学包装成“玄学”或“新范式神话”,而是从工程落地角度拆开来看:它到底解决了什么问题、核心机制是什么、你自己能不能复现类似的缩减效果、以及哪些场景下不建议使用。
1. 语义热力学到底在研究什么
先给一个基本判断:语义热力学不是压缩算法,也不是 Prompt 工程技巧,而是一种分析和约束 LLM 语义生成空间的系统方法。
它借用了热力学中的几个概念帮助我们理解大模型的生成行为:
- 语义状态空间:所有可能输出语料的集合。模型每生成一个 token,就是在状态空间中走一步。
- 语义熵:生成结果的不确定性程度。同样的信息,可以用高熵的松散表达,也可以用低熵的紧凑表达。
- 语义温度:并不完全等同于模型采样参数里的 temperature,而是指一段文本在表达信息时的“活跃度高不高”——形容、铺垫、重复、绕弯越多,温度越高;直接、精准、紧凑,温度就越低。
- 叙事约束:在进入生成过程之前,对模型可以选择的叙事路径预先施加限制,相当于把语义状态空间提前缩小。
把这几件事串起来,就得到了语义热力学想表达的模型:大模型的 token 消耗,本质上取决于它被允许在语义状态空间里“漫游”的范围。范围越大,熵越高,说废话的余地就越多;范围越小,输出的信息密度越高,token 消耗自然下降。
传统 Prompt 设计往往靠“多说几句”来让模型听懂,本质上是在诱导模型走向某个区域,但并没有阻止它在区域内部绕路。而叙事约束解决的是后者:把内部行走的路径也规定好。
2. 为什么 Token 膨胀会成为应用瓶颈
在聊解决方案之前,先量化一下问题到底有多严重。很多人只盯着 API 单价,但 token 膨胀带来的成本是乘数级的。
2.1 推理成本与 Token 数量是线性关系
只要调用 API,无论用哪家模型,费用都近似等于“输入 token 数 × 输入单价 + 输出 token 数 × 输出单价”。而输出 token 往往比输入 token 更贵。如果一个任务原本只需要 200 token 就能完成,但因为模型在“组织语言”,最终输出了 900 token,成本就平白多出 4 倍。
2.2 KV Cache 放大问题
做流式输出或长对话时,模型需要缓存历史的 Key-Value 状态。你生成的废话越多,KV Cache 就越大,显存占用越高,单机并发数越低。这就是为什么很多人发现,换了个便宜的模型,反而把 GPU 显存打满了——不是模型参数变大了,而是输出 token 太多。
2.3 多轮 Agent 场景里 Token 会指数级积累
在 Agent 或多步骤工具调用的场景中,每一轮输出都会拼到下一轮的输入里。假设每轮多出 400 个废话 token,执行 5 轮工具调用,累积到最后一轮的输入就可能多出 1600 token。如果是 10 轮,就是 3600 token。Token 膨胀在 Agent 链路里不是加法,是乘法。
2.4 现有方案的局限
- Prompt 压缩:把输入压短,但模型照样可以在输出端自由发挥。
- Max Tokens 截断:一刀切,容易截掉关键结论。
- Semantic Cache:可以加速重复任务,但解决不了首次生成的冗余问题。
所以,真正值得动手的空间,是生成端的冗余控制。而叙事约束正好从这一层入手。
3. 核心概念:叙事约束是什么
叙事约束(Narrative Constraints)的基本思路是:在生成开始前,定义一套关于“模型如何表达”的规则集合,要求模型只能在规则圈定的表达方式中完成输出。
它不是内容层面的禁止,而是表达层面的规定。也就是说,我们依然让大模型去做判断、归纳和推理,但我们要限制它“叙述”这些结果时的自由度。
3.1 四类常见的叙事约束
| 约束类型 | 作用 | 示例 |
|---|---|---|
| 结构约束 | 规定输出的形式骨架 | 只输出 JSON,固定字段名,禁止 Markdown |
| 语义约束 | 规定必须出现和禁止出现的信息 | 禁止铺垫,直接给结论,不解释背景 |
| 语言风格约束 | 规定遣词造句的力度和密度 | 用短句,每句话不超过 12 个字 |
| 上下文协议约束 | 规定信息与上下文的关系 | 不重复用户问题,不总结历史,只输出增量 |
3.2 叙事约束与传统系统提示词的区别
很多人会问:这不就是更严格的 system prompt 吗?
区别在于系统性。传统 system prompt 告诉模型“你是谁、做什么”,但没对生成路径施加可度量的约束。而叙事约束的目标非常明确:把语义熵降下来,让输出的 token 分布更集中,并且这种降低是可以量化的。
举一个场景对比。
没有叙事约束时的输出:
好的,我来帮您分析一下这段日志。首先,我们看到系统在 10:32 的时候产生了一个错误,这个错误类型是连接超时。连接超时一般来说意味着上游服务没有及时响应,可能的原因包括网络波动、服务负载过高、或者是防火墙配置问题。综合来看,建议您从这几个方向逐一排查。如果需要,我还可以帮您进一步检查相关配置。总 token 数约 130。信息含量其实就一句:连接超时,建议查网络、负载和防火墙。
加入叙事约束后的输出:
{ "time": "10:32", "error_type": "connection_timeout", "suggestion": ["check_network", "check_load", "check_firewall"] }总 token 数不到 35。信息含量完全一致,Token 用量下降了 70% 以上。
这就是叙事约束的杠杆效应——大模型的语言生成能力完全可以做到直接给结论,但我们默认允许它绕路,于是它真的绕了。
4. 环境准备与实验目标
要把叙事约束落地验证,不需要改造模型,通过 Prompt 和 Parser 层就能实现。下面给出一套可复现的实验思路。
4.1 实验目标
在文本总结类任务上,对比“无约束模式”和“叙事约束模式”的 token 消耗,并验证语义保持度是否达标。
4.2 推荐环境
| 项目 | 建议 |
|---|---|
| 操作系统 | Linux / macOS / Windows 均可 |
| Python 版本 | 3.10+ |
| LLM 访问方式 | 任意 OpenAI 兼容 API,或本地部署模型 |
| 依赖库 | openai,pydantic,tiktoken |
版本细节以你自己的环境为准。本文的重点是通用思路,不绑定特定版本。
pip install openai pydantic tiktoken如果你使用的是本地模型,例如通过 vLLM 或 Ollama 或其他推理框架启动的 OpenAI 兼容服务,代码中只需要修改base_url。
4.3 基础调用结构
# 文件路径:llm_client.py from openai import OpenAI client = OpenAI( api_key="your-api-key", base_url="https://your-endpoint.example.com/v1" # 如使用云端或本地代理 ) def chat(messages, temperature=0.3): resp = client.chat.completions.create( model="your-model-name", messages=messages, temperature=temperature ) return resp.choices[0].message.content这段封装的chat函数之后会被两个模式的实验共用。
5. 核心实现:两种模式的对比实验设计
接下来我们设计一个最小可行实验。任务设定为:对一段线上业务日志做诊断总结。
同一份输入日志,分别用普通模式(A/B)和叙事约束模式(C/D)处理,然后对比 token 数。
5.1 文本 A:无约束模式
请总结以下日志的关键信息和故障原因: [日志片段] 11:23:01 ERROR OrderService timeout calling PaymentService after 5000ms 11:23:02 WARN PaymentService unavailable, retry 1/3 11:23:05 ERROR OrderService timeout calling PaymentService after 5000ms 11:23:10 ERROR OrderService fallback triggered for order 88213使用普通提示词:
# 文件路径:example_no_constraint.py from llm_client import chat LOG = """ 11:23:01 ERROR OrderService timeout calling PaymentService after 5000ms 11:23:02 WARN PaymentService unavailable, retry 1/3 11:23:05 ERROR OrderService timeout calling PaymentService after 5000ms 11:23:10 ERROR OrderService fallback triggered for order 88213 """ messages = [ {"role": "system", "content": "你是一个运维助手。"}, {"role": "user", "content": f"请总结以下日志的关键信息和故障原因:\n{LOG}"} ] response = chat(messages) print(response)这种写法下,模型会输出一段较长的自然语言说明,比如先复述日志、再分析原因、再给建议、再补一段总结。这就是 token 膨胀的主要来源。
5.2 文本 B:叙事约束模式
约束协议用 JSON 形式定义:
{ "output_schema": { "type": "object", "properties": { "error_type": {"type": "string"}, "repeated": {"type": "integer"}, "root_cause": {"type": "string"}, "suggestion": {"type": "string"} }, "required": ["error_type", "root_cause", "suggestion"] }, "rules": [ "不要复述日志原文", "不要输出任何解释性开场白", "root_cause 必须不多于 15 个字", "suggestion 必须不多于 20 个字", "禁止使用 markdown 格式", "只输出 JSON,不要输出其他文字" ] }# 文件路径:example_with_constraint.py import json from llm_client import chat LOG = """ 11:23:01 ERROR OrderService timeout calling PaymentService after 5000ms 11:23:02 WARN PaymentService unavailable, retry 1/3 11:23:05 ERROR OrderService timeout calling PaymentService after 5000ms 11:23:10 ERROR OrderService fallback triggered for order 88213 """ constraint_protocol = { "output_schema": { "type": "object", "properties": { "error_type": {"type": "string"}, "repeated": {"type": "integer"}, "root_cause": {"type": "string"}, "suggestion": {"type": "string"} }, "required": ["error_type", "root_cause", "suggestion"] }, "rules": [ "不要复述日志原文", "不要输出任何解释性开场白", "root_cause 必须不多于 15 个字", "suggestion 必须不多于 20 个字", "禁止使用 markdown 格式", "只输出 JSON,不要输出其他文字" ] } messages = [ {"role": "system", "content": ( "你是一个严格的日志诊断引擎。请遵守以下约束协议输出:\n" f"{json.dumps(constraint_protocol, ensure_ascii=False)}" )}, {"role": "user", "content": f"分析日志:\n{LOG}"} ] response = chat(messages) print(response)这里的关键逻辑是:把约束协议放到 system 消息中,而不是放在 user 消息末尾。原因是 user 消息末尾的内容更容易被后续对话改写,system 位置的约束对生成起始阶段的“引导”作用更强。
5.3 文本 C:Token 统计对比工具
用 tiktoken 统计两种模式的输出 token:
# 文件路径:count_tokens.py import tiktoken def count_tokens(text: str) -> int: encoding = tiktoken.get_encoding("cl100k_base") return len(encoding.encode(text)) if __name__ == "__main__": no_constraint_output = open("output_no_constraint.txt", encoding="utf-8").read() constraint_output = open("output_constraint.txt", encoding="utf-8").read() n1 = count_tokens(no_constraint_output) n2 = count_tokens(constraint_output) reduction = (1 - n2 / n1) * 100 print(f"无约束输出 token:{n1}") print(f"约束输出 token:{n2}") print(f"缩减比例:{reduction:.2f}%")注意:cl100k_base编码器适用于 GPT 系列模型。如果你使用的模型自带 tokenizer,请以该模型官方 tokenizer 为准。不同编码器统计结果会有差异,但这不影响相对对比的结论。
6. 运行结果与效果验证
在我的实验预设中,无约束模式输出的诊断大约在 120 到 180 token,而叙事约束模式输出的 JSON 通常稳定在 30 到 45 token。这个量级与标题中的 79% 缩减比例是一致的——但必须说明,79% 是在论文/实验设定下得到的数字,不代表所有任务都能复现这个比例。
实际跑完会得到类似输出:
{ "error_type": "payment_service_timeout", "repeated": 2, "root_cause": "PaymentService 连续超时", "suggestion": "检查依赖服务可用性,扩容或降级" }这一步需要确认三件事:
- Token 缩减是否真实:统计工具输出中
reduction是否达到预期范围。 - 输出是否可解析:返回结果是否严格是合法 JSON,能否直接
json.loads。 - 语义是否保留:把约束模式的输出给另一个评估模型或人工看,确认
root_cause和suggestion的准确性。
如果缩减不到 50%,大概率是约束协议被模型忽略了。常见原因是 system prompt 太长,模型注意力分散,或模型本身 JSON 输出能力较弱。此时可以把约束规则压缩到 4 条以内,并采用“先给示例,再给任务”的方式。
7. 语义保持度与 Token 缩减的权衡
Token 缩减不是唯一目标,语义保持度才是底线。如果为了省 token 把关键信息丢掉,那就会变成捡芝麻丢西瓜。
语义保持度可以从三个维度评估:
| 维度 | 说明 | 评估方式 |
|---|---|---|
| 信息完整性 | 关键实体、结论是否都在 | AI 评委打分或人工标注 |
| 事实一致性 | 没有新增原文不存在的信息 | 抽查约束输出与原文比对 |
| 可执行性 | 结果能否直接被下游系统使用 | 尝试json.loads后交给下游程序 |
需要注意的是,叙事约束对精确信息的保留能力有限。如果日志包含订单号、IP 地址、金额等关键字段,不要指望模型自觉放进 JSON。应该在约束协议中显式列出必须保留的字段,否则模型可能会在压缩时省略这些“看似不重要”的信息。这一点也是我在实验材料看到最典型的踩坑点。
8. 适用场景与边界条件
8.1 高收益场景
- 日志摘要与告警诊断:系统日志本身信息密度低、重复度高,非常适合叙事约束。
- RAG 检索结果压缩:检索回来的文档片段往往很长,先用约束模式压成摘要,再交给最终模型,可以显著降低输入成本。
- Agent 中间状态记录:工具调用的观察结果用 JSON 约束输出,既省 token,又方便代码解析。
- 多轮对话历史摘要:把历史对话按约束协议压缩成结构化状态,能明显降低长对话的成本。
8.2 不适合的场景
- 法律文书、合同、医疗记录:这类场景要求逐字准确,任何压缩都可能引入风险。
- 创意写作、营销文案:温度高了才好出内容,约束会扼杀生成多样性。
- 需要完整解释链路的场景:如果你想让用户读懂完整推理过程,别压缩,交互动体验会变差。
一个更稳妥的判断是:叙事约束适合解决“模型说太多”的问题,不适合解决“模型不知道”的问题。如果模型本身知识不足,约束只会让错误变得更紧凑。
9. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 输出不是合法 JSON | 模型对 JSON 语法掌握不足 | 用校验脚本检查json.loads | 改用结构化输出能力更好的模型,或在协议中给出一个 JSON 示例 |
| 缩减比例远低于预期 | 约束被模型忽略,生成了额外解释 | 查看 system prompt 长度和规则清晰度 | 压缩规则条数,把最重要的规则放在前两句 |
| 关键字段被丢失 | 约束协议没有显式列出必须保留字段 | 对比压缩前后的关键实体 | 在协议中增加must_include_fields列表 |
| 生成质量下降明显 | 约束过强,模型为省 token 牺牲语义 | 人工评估语义保持度 | 放宽字数限制,允许少量补充字段 |
| 中文 token 统计偏差大 | 编码器与模型不匹配 | 检查 tokenizer 来源 | 用模型官方 tokenizer 重新统计 |
10. 最佳实践与工程建议
10.1 约束协议版本化
叙事约束协议会快速迭代,建议把它当成像 API Schema 一样管理,使用版本号标注:
{ "version": "1.2", "constraints": [...] }每次变更都记录增量。否则模型一换,之前调好的约束效果可能就变了。
10.2 与结构化输出双保险
如果你用的模型支持 JSON Schema 模式、或 OpenAPI 函数调用,可以在跑叙事约束的同时开启模型自带的结构化输出。两者叠加的效果通常更好:模型侧保证语法合法,叙事约束保证语义紧凑。
10.3 监控 Token 消耗
生产环境建议把 token 统计做成中间件,记录每次调用的输入和输出 token 数,设置环比告警。一旦某次 Prompt 调整导致输出 token 暴涨,能第一时间发现。
10.4 安全与合规提醒
- 不要用叙事约束去压缩包含用户隐私的原始数据后再存储,压缩后的 JSON 可能仍然包含敏感信息。
- 在正式应用前,在小流量上做语义保持度对比评估,不要直接全量切换。
- 保留一份无约束模式的输出链路作为回滚方案。
10.5 从单点实验到全链路改造
先选一个高重复度、高 token 消耗的单点任务验证效果,跑通后再推广到 RAG 管线或 Agent 中间状态。不要一上来就把所有业务切换到约束模式。
11. 总结与后续学习方向
语义热力学和叙事约束带来的,不是一套花哨的新名词,而是一个被很多人忽略的优化视角:LLM 的 token 消耗,不仅是模型参数和输入长度决定的,还和它被允许的“表达自由度”强相关。
用叙事约束做生成端优化,本质上是把模型从“一个话多的实习生”变成“一个只说重点的资深同事”。这个转变不需要修改模型权重,不需要重新训练,只需要一套好的约束协议和配套解析代码。从工程角度来看,性价比非常高。
如果你想深入这个方向,下一步值得研究:
- 约束协议的自动生成:能不能让模型根据任务类型自动生成最简约束?
- 熵值估算工具:如何实时估算一段输入的语义熵,并自动决定是否需要施加约束?
- 跨模型迁移:同一套约束在 GPT 系列表现好,在本地开源模型上效果如何?
- 与语义缓存叠加:约束输出更紧凑,缓存命中率是否也会因此提升?
建议你先拿自己线上一个高消耗的文本摘要类任务跑一次前面对比实验,用真实数据判断叙事约束在你的场景里的 ROI。毕竟,79% 是别人实验里的数字,你自己的业务场景能省多少,跑过才知道。