利用大语言模型实现确定性编程
基本信息
- 作者: todsacerdoti
- 评分: 3
- 评论数: 0
- 链接: https://www.mcherm.com/deterministic-programming-with-llms.html
- HN 讨论: https://news.ycombinator.com/item?id=47158834
导语
随着大语言模型在代码生成领域的应用日益深入,如何确保输出的一致性与可复现性,已成为工程化落地中的关键挑战。本文探讨了确定性编程的概念与实践,分析了从温度控制到结构化输出的多种技术路径。通过阅读本文,开发者可以掌握消除模型随机性的具体策略,从而在复杂系统中构建更加稳定、可控的智能应用。
评论
中心观点 文章主张通过结构化约束与逻辑编排(Deterministic Programming)来封装大语言模型(LLM)的非确定性本质,从而构建出兼具模型生成能力与传统软件可靠性的智能系统,这是AI工程化从“玩具”走向“工业级”的必经之路。
支撑理由与边界分析
概率模型与确定性行为的矛盾统一
- 支撑理由(事实陈述/作者观点): LLM 的底层机制是基于概率的下一个 Token 预测,这导致其输出具有天然的不稳定性。文章提出的“确定性编程”并非改变模型底层的概率属性,而是通过工程手段(如 Structured Output、类型约束、工作流编排)在系统的输入输出层面建立确定性契约。这是解决 LLM 落地生产环境“最后一公里”问题的关键。
- 反例/边界条件(你的推断): 在纯粹的创意写作或开放域聊天场景中,过度强调确定性会扼杀 LLM 的发散性思维优势,导致输出僵化。此外,对于极度复杂的逻辑推理任务,仅靠外部约束可能无法完全消除模型内部的“幻觉”风险,确定性外壳可能掩盖内部逻辑的脆弱性。
从 Prompt Engineering 到 Software Engineering 的范式转移
- 支撑理由(作者观点/行业共识): 文章暗示了仅靠 Prompt(提示词)无法构建复杂应用。必须引入变量、控制流、函数调用等传统编程概念。TypeScript、Pydantic 等强类型语言与 LLM 的结合(如 TypeChat、Vercel AI SDK),标志着 AI 开发正在回归软件工程的严谨轨道,即通过代码逻辑来“驯服”模型。
- 反例/边界条件(你的推断): 这种范式转移增加了开发的认知负荷和系统复杂度。对于简单的 MVP(最小可行性产品)验证,过度设计的工程化架构可能导致开发效率下降,违背了 AI 原本追求的低门槛敏捷开发初衷。
可观测性与调试的必要性
- 支撑理由(事实陈述): 传统的黑盒调用无法满足企业级应用的需求。确定性编程强调中间步骤的可见性和状态的显式管理,使得系统具备可追溯性和可调试性,这是构建可信 AI 系统的基石。
- 反例/边界条件(你的推断): 引入过多的中间检查点和状态管理可能会显著增加系统的延迟和 Token 消耗成本。在实时性要求极高的交互场景中,这种严谨性可能成为性能瓶颈。
深入评价
1. 内容深度与论证严谨性 文章触及了当前 AI 应用层的核心痛点。其论证的深度在于它没有停留在“模型越大越好”的算力军备竞赛层面,而是深入到了“如何用好模型”的系统架构层面。文章将 LLM 视为 CPU 中的 ALU(逻辑算术单元)或数据库引擎,而非应用程序本身,这一类比非常精准且具有启发性。论证严谨性体现在它指出了单纯依赖 Prompt 的脆弱性,强调了“代码即法律”在 AI 领域的回归。
2. 实用价值与创新性 该观点具有极高的实战价值。目前业界头部趋势(如 LangChain、LlamaIndex 的演进,以及 OpenAI 的 GPTs 结构化)都在印证这一方向。
- 创新性: 它并非发明了全新的技术,而是提出了一种架构思维的重组。它打破了“AI 工程师 = Prompt 工程师”的浅层认知,确立了“AI 工程师 = 全栈软件工程师”的新标准。
- 案例说明: 以 TypeScript + Zod 为例,开发者定义数据结构,LLM 负责填充内容,编译器负责校验。这种模式彻底解决了 JSON 解析错误这一常见痛点,直接将 LLM 应用纳入了现有的 CI/CD 流水线。
3. 可读性与行业影响 文章逻辑清晰,术语使用准确,很好地平衡了理论高度与工程实践。
- 行业影响: 这篇文章(或此类观点)正在推动开发工具链的变革。它预示着未来 IDE 和框架将更深层次地集成 LLM 能力,不是作为聊天窗口,而是作为带有类型感知的 API 组件。这将加速淘汰那些仅靠“魔咒”般 Prompt 维护的脆弱项目,促使行业建立更严格的 AI 交付标准。
4. 争议点与不同观点
- 过度工程化风险: 部分研究者认为,随着模型推理能力的提升(如 o1 系列模型),模型本身将具备更强的自我纠错和规划能力,外部强约束可能变得多余,甚至限制了模型智能的上限。
- Agent 与 DAG 之争: 确定性编程通常基于 DAG(有向无环图)或固定流程,而当前火热的 Agent(智能体)概念强调自主性和动态决策。如何在保证系统稳定性的同时,不限制 Agent 的自主探索,是一个未解决的难题。
实际应用建议
- 采用“三明治”架构: 底层是传统代码逻辑(确定性强),中间层是结构化调用层(TypeScript/Pydantic 约束),顶层才是 LLM(概率性)。不要试图用 LLM 做它不擅长的精确算术或状态保持。
- 防御性编程: 假设 LLM 的输出永远是不合规的。在代码中编写重试机制、Fallback(降级)策略以及严格的输出校验器。
- 工具选型:
代码示例
| |
| |
| |
案例研究
1:Cognition 公司的 Devin AI 软件工程师
1:Cognition 公司的 Devin AI 软件工程师
背景: Cognition AI 致力于开发世界上第一个 AI 软件工程师 Devin。为了使 Devin 能够真正自主地完成复杂的编码任务,而不仅仅是生成代码片段,系统必须具备长远的规划能力和对运行环境的精确控制。
问题: LLM 本质上是概率性的,直接生成的代码往往包含语法错误、逻辑漏洞或依赖缺失。如果仅仅依靠 LLM 生成代码并直接运行,会导致频繁的崩溃和不可预测的行为。如何将 LLM 的代码生成能力转化为一个可靠、可复现且能够自我修复的工程系统,是核心挑战。
解决方案: Cognition 构建了一个确定性的执行引擎,将 LLM 封装在严格的“规划-执行-验证”循环中。LLM 不再直接输出最终代码,而是输出结构化的“思维链”和具体的工具调用指令(如编辑特定行号的代码、运行特定的终端命令)。系统会在每一步执行后捕获输出(如编译错误、测试失败信息),然后确定性地将这些错误反馈给 LLM,要求其进行针对性的修复。只有当所有测试通过且系统状态符合预期时,任务才被视为完成。
效果: 这种确定性的编程方式使得 Devin 能够成功完成 Upwork 上的真实工作任务,例如运行端到端的测试、修复遗留代码库中的 Bug 以及部署 Web 应用。它显著降低了 AI 生成代码的幻觉风险,将 LLM 从一个“聊天机器人”转变为一个可信赖的工程助手,能够处理多步骤、长周期的复杂开发任务。
2:Windsurf IDE(由 Codeium 开发)
2:Windsurf IDE(由 Codeium 开发)
背景: 随着 AI 辅助编程的普及,开发者面临着 IDE(集成开发环境)与 AI 模型之间上下文割裂的问题。开发者希望 AI 不仅能补全当前行的代码,还能理解整个项目的文件结构、依赖关系和定义。
问题: 传统的 Copilot 模式是被动的,且缺乏对项目全局状态的确定性感知。LLM 往往不知道某个函数是在哪里定义的,或者不知道修改一个文件会如何影响其他文件。这种“盲盒”式的生成导致建议的代码经常在上下文不一致的情况下无法运行。
解决方案: Windsurf 引入了“Flows”技术和 Deep Context Understanding(深度上下文理解)。它构建了一个确定性的索引层,将 LLM 与 IDE 的文件系统、符号表和定义-引用图实时连接。当开发者触发 AI 操作时,系统不是让 LLM 盲目猜测,而是首先通过确定性算法提取相关的项目上下文(如相关的类定义、接口文档),然后将这些精确的信息填充到 LLM 的 Prompt 中。这使得 LLM 的输出被严格限定在项目的实际约束范围内。
效果: 这种结合使得 AI 能够进行跨文件的重构、理解复杂的代码库逻辑,并生成与现有代码风格完全一致的代码。开发者反馈表明,这种确定性的上下文感知能力极大地减少了“幻觉”代码的产生,让 AI 真正成为了能够理解项目全貌的结对编程伙伴,而不仅仅是一个自动补全工具。
3:Klarna 的客服自动化系统
3:Klarna 的客服自动化系统
背景: Klarna 是一家瑞典的金融科技巨头,每天需要处理海量的客户服务咨询,涉及退款、退货、账户管理等具体业务流程。
问题: 直接使用通用的 LLM(如 GPT-4)来回答客户问题是非常危险的。模型可能会根据概率生成错误的退款金额、承诺不存在的服务,或者编造公司的政策条款。这种非确定性的输出在金融领域是不可接受的,会导致合规风险和财务损失。
解决方案: Klarna 并未直接依赖 LLM 的生成能力来回复,而是实施了“LLM 作为控制器”的确定性编程模式。他们构建了一个严格的系统,其中 LLM 的作用是理解用户的自然语言意图,并将其映射为后台 5000 多个预定义的、经过验证的 API 端点或业务逻辑流程。LLM 不生成具体的业务数据(如金额),而是输出结构化的 JSON 来调用确定性的后端服务。最终回复给客户的内容,是由这些经过验证的后端服务返回的,而非 LLM 即兴编造的。
效果: 据报道,该系统在推出后不久就处理了三分之二的客户咨询(相当于 700 名全职客服的工作量),并将客户咨询的解决时间从 11 分钟缩短至 2 分钟。通过将 LLM 的语义理解能力与确定性的业务逻辑执行相结合,Klarna 在保证准确性和合规性的同时,实现了巨大的效率提升。
最佳实践
最佳实践指南
实践 1:构建结构化提示词
说明: 为了从 LLM 获取一致的输出,必须抛弃随意的对话方式,转而使用高度结构化、模块化的提示词。清晰的指令能减少模型的幻觉,使其行为更具可预测性。
实施步骤:
- 定义明确的角色设定,例如“你是一位资深的数据结构工程师”。
- 使用 XML 标签或分隔符(如
###或""")来区分指令、上下文数据和输入数据。 - 在提示词中显式声明输出格式,例如 JSON、XML 或 Markdown 表格。
注意事项: 避免模糊的指令,如“帮我分析一下”,而应使用“分析以下文本的优缺点,并列出 3 个要点”。
实践 2:实施约束解码
说明: 在生成代码或结构化数据时,LLM 可能会生成语法错误或不完整的片段。通过约束解码技术,可以强制模型输出符合特定语法或正则表达式的内容,确保输出的机器可读性和确定性。
实施步骤:
- 在应用层集成库(如 Python 的
outlines或guidance),这些库允许在生成时对 Token 采样进行掩码操作。 - 定义严格的输出模式,例如 JSON Schema 或正则表达式
r"[A-Z][a-z]+"。 - 在推理循环中应用这些约束,使模型在每一步只能选择符合规则的 Token。
注意事项: 约束解码会略微增加推理时的计算开销,但对于需要 100% 语法正确的场景(如 SQL 生成)是必须的。
实践 3:使用低温度参数与 Top-K 采样
说明: 模型的随机性主要由采样参数控制。为了实现确定性编程,必须将随机性降至最低,使模型在给定相同输入时始终产生相同的输出。
实施步骤:
- 将
temperature设置为 0。这会使模型总是选择概率最高的 Token。 - 谨慎调整
top_p参数,通常建议设为 1 或接近 1,以避免在概率分布截断时引入不必要的波动。 - 如果 API 支持,使用
seed(种子)参数来进一步固定随机数的生成状态。
注意事项: 即使 Temperature 设为 0,不同的模型版本或底层基础设施的浮点运算差异仍可能导致细微变化,因此不要完全依赖此参数处理关键事务逻辑。
实践 4:验证与重试机制
说明: 即使使用了最佳实践,LLM 仍属于概率性系统。确定性编程需要承认这一事实,并在代码层面建立防御机制,通过程序逻辑来验证模型输出并自动修正错误。
实施步骤:
- 编写验证器函数,检查 LLM 的输出是否符合预期格式(如 JSON 是否能被解析,SQL 语法是否正确)。
- 实施自动重试逻辑,当验证失败时,将错误信息反馈给 LLM 并请求重新生成。
- 设置最大重试次数(例如 3 次),超过次数后回退到默认值或抛出异常,避免无限循环。
注意事项: 在重试提示词中包含具体的错误信息,这能显著提高模型自我修正的成功率。
实践 5:少样本学习
说明: 提供具体的示例是引导模型理解复杂任务的最有效方法。通过在提示词中包含“输入-输出”对,可以极大地减少模型对指令的歧义理解,提高输出的一致性。
实施步骤:
- 挑选 3 到 5 个具有代表性的边缘案例和标准案例。
- 在提示词中清晰地展示这些案例的输入和期望输出。
- 确保示例的格式与实际请求的格式完全一致。
注意事项: 示例过多会增加 Token 消耗和延迟,且可能引入混淆,因此需要精选最具代表性的样本。
实践 6:模型版本锁定与容器化
说明: LLM 服务商经常更新模型,这可能导致“模型漂移”,即昨天的正确代码今天突然失效。确定性编程要求严格控制生产环境的模型版本。
实施步骤:
- 在 API 调用中指定具体的模型版本号(例如
gpt-4-0613而不是gpt-4),避免使用自动升级到最新版本的别名。 - 如果使用开源模型,通过 Docker 或类似技术容器化模型推理服务,确保权重文件和推理环境不可变。
- 建立模型评估基准,在新版本部署前进行回归测试。
注意事项: 记录所使用的模型版本 ID,这对于复现 Bug 和审计至关重要。
实践 7:将 LLM 视为最终执行者而非逻辑控制者
说明: 最核心的确定性原则是不让 LLM 编写难以预测的控制流代码。应将 LLM 嵌入到传统的确定性代码逻辑中,作为生成内容或组件的函数,而非主导程序流程。
实施步骤:
- 使用 Python/JavaScript 等传统编程
学习要点
- 基于对“Deterministic Programming with LLMs”(LLM 确定性编程)相关讨论的总结,以下是 5 个关键要点:
- 将 LLM 视为概率组件而非确定性函数,必须在架构层面通过重试循环和验证机制来处理其固有的不稳定性。
- 结构化生成(如 JSON 模式)和类型提示是确保输出可解析性和下游系统稳定性的基础,比单纯的文本提示更可靠。
- 确定性系统的核心在于“评估驱动开发”,即像测试传统代码一样,通过自动化测试集来验证模型行为的准确性和一致性。
- 通过降低温度参数和利用 Log Probs(对数概率)分析,可以有效减少输出的随机性,并识别模型在生成过程中的置信度。
- 针对复杂任务,应采用链式调用将推理过程分解,利用中间步骤的确定性验证来提高最终结果的可靠性。
常见问题
1: 什么是确定性编程,为什么它在 LLM 应用中很重要?
1: 什么是确定性编程,为什么它在 LLM 应用中很重要?
A: 确定性编程是指在给定相同输入的情况下,程序总是产生相同输出的特性。在传统软件工程中,这是基本要求,但在基于大语言模型(LLM)的应用中,由于模型生成的随机性,默认情况下每次调用可能会返回不同的结果。这对于企业级应用、自动化测试、数据处理管道以及需要可复现性的研究场景来说是不可接受的。实现确定性编程意味着开发者可以像调用普通函数一样可靠地调用 LLM,从而将 AI 能力安全地集成到复杂的软件系统中。
2: 如何在代码中实现 LLM 输出的确定性?
2: 如何在代码中实现 LLM 输出的确定性?
A: 实现 LLM 确定性的核心在于控制“温度”参数和设置随机种子。具体步骤如下:
- 设置 Temperature 为 0:这是最关键的一步。温度参数控制输出的随机性。将其设置为 0(或非常接近 0 的值,如 1e-8)会使模型在每一步选择概率最高的 token,从而消除随机性。
- 固定 Seed(种子):虽然 Temperature 为 0 是主要因素,但在某些底层实现或特定模型架构中,设置固定的随机种子(
seed)可以进一步确保在不同运行间的一致性。 - 版本控制:确保每次调用使用相同的模型版本。因为模型微调或版本更新可能会导致相同输入产生不同输出。
3: 即使设置了 Temperature 为 0,为什么结果有时仍然不同?
3: 即使设置了 Temperature 为 0,为什么结果有时仍然不同?
A: 这是一个常见问题,通常由以下原因导致:
- 浮点数精度与硬件差异:在不同的硬件(如不同型号的 GPU)或不同的深度学习框架上运行推理时,浮点运算的微小精度差异可能会累积,导致在多个候选 token 概率非常接近时,模型选择了不同的 token。
- 模型负载与分片:在云端 API(如 OpenAI, Anthropic)中,请求可能会被路由到不同的计算集群或模型分片上。这些分片虽然权重相同,但物理实现上的细微差别可能导致输出不一致。
- Top-k 或 Top-p 采样:如果除了 Temperature 之外,还启用了 Top-p(nucleus sampling)或其他采样策略,可能会在边界情况下引入不确定性。通常建议在追求极致确定性时,关闭其他采样干扰。
4: 除了模型参数,还有哪些因素会影响 LLM 的输出一致性?
4: 除了模型参数,还有哪些因素会影响 LLM 的输出一致性?
A: 输入的标准化是另一个关键因素:
- Prompt 格式化:Prompt 中多余的空格、换行符或不可见字符都会被视为输入的一部分,从而改变输出。开发者需要严格标准化 Prompt 模板。
- 系统提示词:如果系统提示词发生变化,或者用户输入与系统提示词的交互方式不同,结果会大相径庭。
- 上下文截断:如果输入长度接近模型的上下文窗口限制,细微的长度差异可能导致截断位置不同,进而丢失关键信息,导致输出变化。
5: 在追求确定性的同时,如何平衡模型的创造性和多样性?
5: 在追求确定性的同时,如何平衡模型的创造性和多样性?
A: 确定性通常意味着牺牲多样性。解决这一矛盾的最佳实践是分离关注点:
- 结构化输出:对于需要严格逻辑和格式的部分(如 JSON 生成、SQL 查询、数据提取),使用 Temperature = 0 的确定性模式。
- 创意生成:对于需要发散思维的内容(如头脑风暴、文案写作),使用较高的 Temperature(如 0.7 - 1.0)。
- 两阶段生成:第一阶段使用确定性模型生成大纲或逻辑骨架,第二阶段使用高温度模型填充细节和润色语言。这样既保证了逻辑的稳固,又保留了内容的丰富性。
6: 确定性编程对 LLM 应用的成本和性能有什么影响?
6: 确定性编程对 LLM 应用的成本和性能有什么影响?
A: 确定性编程主要影响开发和运维效率,对运行时的计算成本影响极小:
- 缓存优势:这是确定性的最大优势。如果输入和输出是一一对应的,开发者可以引入结果缓存。对于重复的查询,可以直接返回缓存结果而无需调用模型,这能大幅降低 API 成本并显著提高响应速度。
- 调试与测试:确定性使得单元测试成为可能。开发者可以断言特定输入必然产生特定输出,极大地简化了 CI/CD 流程和错误排查过程。
7: Structured Output(结构化输出)与确定性编程有什么关系?
7: Structured Output(结构化输出)与确定性编程有什么关系?
A: 结构化输出(如强制模型返回 JSON 或 Pydantic 对象)是确定性编程在数据格式层面的延伸。仅仅保证文本的确定性是不够的,如果下游程序需要解析数据,模型的输出必须符合严格的语法规则。
- 结合使用:通常做法是同时设置 Temperature = 0 并使用 Function Calling 或 JSON Mode。这确保了每次不仅内容相同,而且格式完全符合解析器的要求,避免了因格式错误(如缺少闭合括号)导致程序崩溃的风险。
思考题
## 挑战与思考题
### 挑战 1: 结构化输出的鲁棒性解析
背景**:在调用 LLM API 时,要求模型以 JSON 格式返回一个包含 name(字符串)和 age(整数)的对象。然而,模型有时会在 JSON 前后添加 Markdown 代码块标记(如 ```json … ```)或者添加额外的解释性文字。
任务**:请编写一段代码,尝试解析返回的 JSON。设计一套逻辑(包含 Prompt 优化或后处理代码),确保即使模型输出包含上述噪音,程序也能稳定提取到正确的 JSON 对象。
提示**:考虑使用正则表达式来提取 JSON 部分,或者在 Prompt 中明确禁止使用 Markdown 格式。思考如何平衡 Prompt 的约束强度和模型遵循指令的能力。
引用
- 原文链接: https://www.mcherm.com/deterministic-programming-with-llms.html
- HN 讨论: https://news.ycombinator.com/item?id=47158834
注:文中事实性信息以以上引用为准;观点与推断为 AI Stack 的分析。
站内链接
相关文章
- 大语言模型成为新型高级编程语言
- LLM成为新型高级编程语言
- 大语言模型成为新一代高级编程语言
- AI 编程代理已全面替代我使用的所有开发框架
- LLM 作为语言编译器:Fortran 对编程未来的启示 本文由 AI Stack 自动生成,包含深度分析与可证伪的判断。