BLOG · #Engineering

确定性边界:LLM 应用的架构第一原则

LLM 是概率部件,而生产应用需要工程保证。本文提出一条经过实证的架构原则——凡代码能回答的不问模型,模型只产最小片段,片段必经确定性关卡——并给出五个机制的设计细节、量化收益与适用边界。实证基座是一个带 351 个测试的生产级 AI 写作 agent。

English version: The deterministic boundary: a first principle for LLM application architecture。“LLM 应用工程”系列第 11 篇——前十篇各机制在此汇成一条统一原则。本系列以一个生产级 AI 写作 agent(FastAPI + React + Tauri,351 个测试用例)的实现与实测数据为实证基座,讨论可迁移的工程方法。

问题:概率部件,工程责任

把 LLM 嵌进生产应用后,工程上真正的难点不是"模型不够聪明",而是它作为一个概率部件,会以几种可预期的方式破坏系统性质:

  1. **作用域失控。**让模型"改一段",它重写整篇。对整篇重写式修订的实证测量(arXiv:2601.13217)显示,修订平均回退 16–27% 已覆盖的要点——不是模型失误,是开放式重写的固有统计性质。
  2. **循环坍塌。**长上下文下,agent 会退化为以相同参数重复调用同一工具。公开案例(qwen-code issue #4695)记录了单次会话 43 次连续 git status、消耗 8.9M token;SDK 层的重试机制对这类故障无效,因为每次调用在 API 层都是"成功"的。
  3. **成本漂移。**如果每轮交互都携带全量上下文,一个"把关键词改一下"的请求也要支付整篇文档加全部素材的 token——我们实测这类请求的输入开销约 6–7k token,而其中信息量约为 1.5k。
  4. **回归不可测。**输出非确定,传统断言失效;提示词一改,质量静默漂移,CI 无从拦截。

针对这些失效模式,我们在项目里收敛出一条架构原则,本文称之为确定性边界(deterministic boundary):

凡确定性代码能回答的问题,不问模型;模型只产最小必要片段;片段必经确定性关卡才能进入系统状态。

这条原则与业界现有的两类手段正交:structured outputs / function calling 解决的是格式确定性(输出长什么样),guardrails 类框架解决的是内容合规(输出能不能说);确定性边界处理的是作用域与状态确定性(输出被允许改动系统的哪一部分、以什么条件落库)。代码编辑器领域的 str_replace / fast-apply 模式(模型产 diff、应用是确定性的)是同一原则在另一个域的表达。

下文以文档修订场景为例,拆解五个机制。技术栈:Python/FastAPI,文档格式为 LaTeX,但机制本身与格式无关——任何有可解析结构的领域都适用。

机制一:确定性定位——分节器

作用域编辑的前提是能确定性地回答"目标节的字符区间是什么"。我们的分节器(约 260 行,零 LLM、零依赖)把 LaTeX 源码按 \section / \subsection / \subsubsection 解析成节点树,每个节点给出 char_start / char_end。两个设计点值得展开:

屏蔽而不位移。注释和 verbatim 环境里的 \section 是假标题,但如果先删掉它们再解析,所有字符 offset 都会失效。解法是把这些区域替换成等长空格

def _mask(content: str) -> str:
    """把注释与逐字环境替换成等长空格(保持所有字符 offset 不变),
    使其中的 \section 不被当成真标题——但 splice 仍用原文 offset。"""

在掩码文本上定位标题,在原文同一 offset 上读取内容。解析与拼回共用一套坐标系,没有换算,也就没有换算错误。

**双锚点。**每个节点同时携带位置型锚(n0n1,同一版本内稳定)和内容指纹键(L1:误差分析,由层级+标题派生)。位置锚在章节增删后会漂移,内容键跟着标题走、跨版本更稳;定位时内容键优先。这是为"跨版本引用同一节"付出的最小代价——不需要 diff 算法,两个字符串键就够。

失败模式也是确定性的:解析不出结构就返回 editable=False,调用方回退到全文修订。边界机制自身绝不抛异常阻断主流程——它的职责是收窄模型的作用域,不是制造新的故障点。

机制二:作用域生成与确定性拼回

定位之后,发给模型的提示词只含目标节(外加防越界的铁律),模型返回的也只是该节的替换片段。片段回到主文档的路径全部是代码:

  1. strip_fragment:清洗——去 Markdown 围栏;若模型违规返回了整篇文档,切出从首个标题到 \end{document} 之前的部分;
  2. check_fragment:体检——花括号配平、环境配平、无 \documentclass(软信号,提示不硬拦);
  3. splice(content, start, end, new_text):一行拼回,区间外字节级不变

字节级不变是可测试的承诺,不是形容词:对应的断言直接写在测试里。一个实现层的教训:片段必须保持被替换跨度内的顶层 \section 数量,否则同一轮里后续编辑项的位置锚会整体漂移、改错节——这个 bug 是内部对抗评审发现的,修复后成为固定测试项。

机制三:零 token 预路由

“用户这条意见想改哪里"是一个路由问题。默认做法是问模型,但相当一部分意见带有无歧义的显式目标:“参考文献格式不对”“摘要太长”“表2 的单位错了”“§3.1 重写一下”。这类请求用规则直接判定:

信号类别 = { bib 类词, 摘要/关键词, 节引用(表N/图N/§编号/唯一节标题) }
命中恰好一类 → 直接路由(0 token)
无信号 / 跨类冲突 / 含"全文""整体"字眼 → 退给 LLM 路由

设计立场是保守优先:宁可退 LLM,绝不硬猜。歧义处理也是确定性的——“数据处理"和"数据处理与结果"两个标题同时命中时,保留更长的那个。命中路由后配合机制二的窄提示词,实测把"改关键词"一类请求的输入 token 从约 6–7k 降到约 1.5k;路由本身零成本、零延迟,且不需要 API key,离线测试可以全速跑。

机制四:零 token 回归门

自主 agent 的反思环(草稿 → 自我修订 × N)是作用域失控的重灾区。除了把修订约束成结构化编辑计划(schema 强制 changes[],空数组即视为收敛、提前终止)加作用域 splice,落库前还有最后一道闸门 regression_check——六项全部是确定性判据:

  • 丢失可编译骨架(\documentclass / \end{document});
  • 篇幅骤缩到上一版 60% 以下(截断或"其余不变"式省略);
  • 结构分下降(八项等权检查:章节数、误差分析、数据表、公式、无 TODO 占位……);
  • 引用 \cite key 集合变小;
  • 章节数减少;
  • 插图或数据表减少。

任一触发即回退上一版并终止迭代。选择确定性判据而不是再调一次 LLM 评委,理由写在模块注释里:这个失败模式(丢内容)本身可确定性检测,而配对评委在该领域的效度未经验证且每轮多付一次调用——更对,且更省。

闸门自身也有边界 case:参考文献生成排在反思之后,bib 为空时"引用闭合"检查对任何含 \cite 的稿件恒为 False,会把"新增了一处引用"误判成劣化。修复方式是 bib 为空时把该项移出比分、引用是否丢失改由"key 集合非减"单独判定。闸门是代码,代码有 bug;但闸门的 bug 可以用单测钉死,模型的漂移不能——这正是把关卡放在确定性一侧的理由。

机制五:把边界本身纳入 CI——prompt 形状回归门

离线测试跑不了真实 LLM,测不了生成质量,但可以测提示词的形状:作用域提示词是否仍然要求"只改这一节”;无缺口输入下的组装函数输出是否与 golden 文件逐字节一致。有人把提示词改回"输出整篇修订稿”,普通 pytest 立刻红——确定性边界最脆弱的部分(自然语言写成的约束)由此获得了和代码同级的回归保护。配套的还有评分器自身的判别力门:金标准好报告 ≥0.75、坏报告 ≤0.35、分差 ≥0.4,先证明评分器真的编码了质量,才允许它当门。

全景与收益

flowchart TD A[修改请求] --> B{确定性预路由} B -->|命中显式目标| S[作用域提示词:仅目标节] B -->|无信号或跨类冲突| L[LLM 路由] --> S S --> G[LLM 生成节片段] G --> C[确定性清洗与体检] C --> P[splice 拼回:区间外字节不变] P --> H{在环者} H -->|对话式编辑| U[候选台账,用户接受才落库] H -->|自主反思环| R{零 token 回归门} R -->|通过| V[落为新版本] R -->|劣化| X[回退上一版,终止迭代]
机制替代做法实测收益
分节器 + splice让模型整篇重写非目标区域字节级不变;规避 16–27% 内容回退风险
零 token 预路由每条意见都调路由模型显式目标请求 0 token 判定;输入 6–7k → ~1.5k
回归门无条件接受修订 / LLM 评委每轮省一次评委调用;劣化即回退,收敛即停
prompt 形状门人工 review 提示词改动351 个测试中的确定性部分全部离线可跑、可进 CI

适用边界

按惯例,说清楚这条原则什么时候不成立

  1. **前提是领域有可解析的结构。**LaTeX 有 \section,代码有 AST,Markdown 有标题层级;自由散文没有。没有结构锚点,分节器无从谈起——此时确定性边界退化为只剩清洗和闸门两层。
  2. **边界自身是代码,代码有维护成本。**分节器要处理 starred 标题、可选短标题参数、注释与 verbatim 掩码这些角落,我们为这一层写了几十个单测。如果产品形态还在快速变化,过早建边界会让每次变化付两遍成本。
  3. **它保护存量,不创造增量。**边界的价值随"已有资产的价值"增长:防的是第 N 版被改坏,对从零生成第 1 版没有贡献。创造性主导、无既有资产的任务不需要它。
  4. **确定性判据只测得到可测的。**回归门防"变差",不证明"变好";偏好层的质量仍然需要配对评委或人的判断——那是另一篇文章的主题(LLM-as-Judge 的效度工程)。

参考

  • Beyond Single-shot Writing: Deep Research Agents are Unreliable at Multi-turn Report Revision(arXiv:2601.13217)——整篇重写式修订回退 16–27% 已覆盖内容的实证测量;结构化编辑计划与 scoped 修订正是该文点名的解法。
  • PaperOrchestra: A Multi-Agent Framework for Automated AI Research Paper Writing(arXiv:2604.05018)——结构化多 agent 流水线大幅胜过自主基线(+50–68% / +14–38%);其"改好才收、改差就撤"精修闸门报告 79–81% 胜率、0% 变差(该细节取自正文,未独立核验)。
  • Self-Refine: Iterative Refinement with Self-Feedback(arXiv:2303.17651)——反馈必须具体可执行;泛化反馈显著掉分,错误难自检的任务需接入外部信号。
  • qwen-code issue #4695——工具循环坍塌的公开案例(43 次重复调用 / 8.9M token)。