☰
从溯因到闭环:让大语言模型生成可验证的科学假设
2026/10/24 19:23:50 网站建设 项目流程

之前用大模型直接生成科学假设时,经常会遇到一个奇怪的现象:模型给出的解释看起来逻辑严密、术语专业,但一旦拿到真实实验数据里验证,要么完全对不上,要么根本没法检验。问题并不一定出在模型不够强,而在于整个推理链路缺少“接地”机制。本文从一个偏认知科学和 AI 交叉的命题出发——Abduction Without a Body,拆解 Representational Grounding 与 Abduction Loop 这两个核心概念,并用 Python 实现一个最小可运行的科学假设生成闭环系统。无论你是做 AI4Science、智能体开发,还是想在工程里引入自动假设生成能力,这篇文章都能提供一套可直接参考的落地方案。

1. 背景:从“没有身体的溯因”说起

1.1 什么是“没有身体的溯因”

先解释一下标题里的关键词。Abduction 翻译过来是“溯因推理”,它是科学发现中最常见、也最容易被忽视的一种推理方式。简单说,溯因就是“看到一个异常现象,去猜测一个最合理的解释”。

比如你看到桌上有水渍,结合窗户开着、昨晚下了雨,你推测是雨飘进来了。这个过程没有严格的演绎逻辑,也没有大量归纳样本,你只是在找一个“如果这个解释成立,现象就说得通”的假设。

标题里的 Without a Body,直译是“没有身体”。这里不是讨论 AI 有没有机器人躯壳,而是说:如果推理者没有和真实世界发生交互的“行动主体”,那么溯因推理就只是在符号空间里空转。

对大语言模型来说,它可以在没有实验、没有感知、没有反馈的情况下生成大量假设。它看过无数文献,知道“高温通常会加速化学反应”,所以它能写出一句很像样的解释。但这句话并没有和一组真实测量数据建立联系,也没有办法被证伪。这就是论文标题想表达的核心担忧:a body 不是物理外壳,而是指那个能接收世界反馈、并根据反馈修正自身推理的闭环。

1.2 科学家日常推理里的溯因

认知科学里通常把推理分成三种类型,用一张表格可以看得很清楚:

推理类型推理方向例子
演绎推理规则 → 案例 → 结果所有金属加热会膨胀,铁是金属,所以铁加热会膨胀
归纳推理案例 → 结果 → 规则观察到 100 个铁样本加热膨胀,得出“金属受热膨胀”
溯因推理结果 → 规则(解释性猜测)发现金属膨胀,猜测“它可能含有某种热敏感结构”

科学发现的起点往往是溯因。研究者看到一组异常数据,提出一个“会不会是 X 导致的”猜测。但科学之所以成为科学,并不在于提出猜测,而在于猜测之后必须进入实验验证环节。

现实中很多 AI 辅助科研工具只做了前半段:让模型看数据、生成假设、输出报告。至于假设到底对不对、哪里不对、下一轮应该朝向哪个方向修正,完全依赖人去判断。这相当于把溯因推理从完整的科学循环中切了一半下来。

1.3 为什么标题强调 “Without a Body”

强调没有身体,是因为大语言模型本质上没有“被世界纠正”的机制。模型生成文本时,依据的是训练语料里的统计规律,而不是当前实验台上真实数据对它的反馈。

举个例子。给模型一组温度与反应速率数据,它可能会说:“该反应遵循 Arrhenius 行为,活化能大约在 40~50 kJ/mol。”这句话看起来专业,但它没有回答几个关键问题:

  • 你对 40~50 这个范围的置信度来自哪里?
  • 如果预测速率和实测速率差了三倍,你知道该调整哪个参数吗?
  • 你能在一次实验之后自动修正自己的估计吗?

普通线性 prompt 流程做不到这些。它只能生成“一次性假设”,然后静待人类去验证。缺失的环节,正是让假设回到世界中去接受检验、再把误差反馈回推理过程的闭环。这个闭环,就是 Abduction Loop 的核心。

2. 核心概念拆解:Representational Grounding 与 Abduction Loop

2.1 Representational Grounding:表征锚定

Representational Grounding 在中文语境里通常译为“表征接地”或“表征锚定”。它源自认知科学里的符号接地问题(Symbol Grounding Problem),核心争论是:符号的意义,能不能仅仅通过符号之间的关系来定义?

举个简单的例子。如果你从来没吃过苹果,只看文字描述“苹果是红色的、甜的、脆的”,你得到的只是一种语义网络里的位置。只有当“苹果”这个符号和你的视觉、触觉、味觉经验建立联系后,这个符号才真正对你有意义。这就是 grounding,把符号和符号之外的经验世界连接起来。

放到大语言模型时代,这个概念需要重新翻译一下。LLM 内部处理的是 token 和向量,它当然不理解“活化能”的物理意义。但如果我们要让模型辅助科研,就必须让它的输出和外部世界建立可验证的锚点:

  • 模型说“活化能是 45 kJ/mol”,我们就必须能用实验数据来检验这个数值。
  • 模型说“提高压力会增快速率”,我们就必须能用一组不同压力下的实验数据来检验这个趋势。
  • 模型说“这个假设解释力更强”,我们就必须定义“解释力”用什么指标衡量。

所以,Representational Grounding 在工程上不是不可言说的哲学概念,它可以直接落成一句话:模型输出的每一个关键命题,都必须能在某个评估器或实验台上被量化检验。检验不通过,就代表这个表征没有被锚定到真实世界。

2.2 Abduction Loop:溯因闭环

Abduction Loop 是我非常喜欢的一个工程原语,它把科学发现过程抽象成一个循环:

┌────────────────────────────────┐ │ 观测现象 / 异常数据 │ └──────────────┬─────────────────┘ ▼ ┌────────────────────────────────┐ │ 溯因推理:生成候选解释 │ └──────────────┬─────────────────┘ ▼ ┌────────────────────────────────┐ │ 实验 / 仿真:检验该解释 │ └──────────────┬─────────────────┘ ▼ ┌────────────────────────────────┐ │ 误差反馈:接地评估 │ └──────────────┬─────────────────┘ ▼ 回到“溯因推理”,修正假设

这个循环的每一步都对应真实科研活动:

  • 观测:收集数据,发现异常。
  • 溯因:根据异常提出一个或多个可检验解释。
  • 实验:设计并执行实验,检验解释的预测。
  • 误差反馈:把实验结果与解释预测之间的偏差转换成结构化信息。
  • 修正:带着误差信息重新进行溯因推理。

为什么强调它是 Loop 而不是 Pipeline?因为 Pipeline 是单向的,一次性执行完就结束了;Loop 则允许系统从自己的错误中学习。这正是“有身体的溯因”和“无身体的溯因”的本质区别:一个能跌倒后爬起来,一个只能站在原地描述跌倒的物理过程。

2.3 两个概念如何协同工作

Representational Grounding 和 Abduction Loop 不是两个独立概念,而是一个整体。

  • Abduction Loop 是流程骨架,它规定了系统如何循环:假设、检验、反馈、再假设。
  • Representational Grounding 是每一轮循环的质量标准,它决定了检验是否真正有效、反馈是否是结构化的、假设是否真正和现实世界建立了锚点。

打个比方。Abduction Loop 像投篮训练的动作循环:投球、看落点、调整姿势、再投。Representational Grounding 则是判断“球是否进筐”的标准。如果标准缺失,循环再完美也只是原地重复;如果标准错误,系统会稳定地投到错误方向。

在工程实现中,Grounding 通常表现为一个可量化的指标,比如预测误差、对数似然、覆盖率。有了这个指标,LLM 的下一轮推理就不是听天由命,而是被误差信号引导着朝真实机制收敛。

3. 为什么要把这条闭环工程化

3.1 传统 LLM 假设生成是线性管线

目前很多 AI 科研辅助系统的设计思路仍然是线性管线:

数据预处理 → 构造 Prompt → 调用 LLM → 生成假设 → 人工评估

这种设计最大的问题在于:模型生成完假设,它的任务就结束了。假设到底对不对、误差有多大、下一轮应该怎么调整,模型完全不知道。

你可以把这种模式理解为“闭卷考试”。模型写完答案就交卷,之后分数如何、错在哪里,它一概不知。当你要求它继续优化同一个问题的时候,它只能重新盲猜一次,而不是基于上一次的错误做针对性修正。

3.2 闭环带来的三个收益

把线性管线改造成闭环之后,收益非常直接:

第一,可证伪。每个假设都必须在评估器上产生一组可以比较的数值预测,误差可以被量化。这比让人类看一段“模型分析得有没有道理”要可靠得多。

第二,可回溯。每一轮循环里的假设、实验、误差都作为记录被保存下来。你随时可以回答“为什么系统最后给出了这个假设”,因为它能看到完整的演化轨迹。

第三,可迭代。评估器产生的误差不是终点,而是下一轮 Prompt 的一部分。这意味着系统具备自主改进能力,不需要人工在每轮循环中介入。

3.3 典型应用场景

这种闭环在真实场景里已经有很强的落地价值。

材料领域经常要处理“配方-性能”关系。给定一批已知性能数据,系统生成几个候选配方假设,然后用仿真软件评估性能,再根据性能误差反馈调整下一轮配方假设。

设备故障诊断是另一个典型场景。监控系统发现某个参数异常,LLM 生成“可能是冷却系统故障”等候选原因,随后调用设备仿真模型验证该原因是否会导致相同的异常特征,再把验证结果作为新的约束反馈给模型。

生物通路推断也类似。根据基因敲除实验的表型数据生成机制假说,用新一轮实验去检验假说中的关键预测。只不过在这个领域,评估器往往是昂贵且有限制的真实实验,因此更需要“先仿真筛选,再实验确认”的分级闭环策略。

这些场景有一个共同前提:必须存在一个可信的、相对快速的评估器,无论是仿真器、规则引擎还是真实实验接口。没有评估器,Abduction Loop 就只能退化成人工评审循环,那又回到了线性管线。

4. 实战:用 Python 构建一个 Abduction Loop 最小系统

4.1 场景与目标

为了把前面这些概念落到实处,这里设计一个受控的玩具实验环境。

假设我们有一个化学反应实验台。实验台内部遵循一个我们“看不见”的真实动力学模型,该模型基于 Arrhenius 公式,并带有一个已知的压力修正系数。观测系统会读取温度、压力和反应速率,并叠加测量噪声产生实验数据。

系统要做的事情是:只看观测数据,通过多轮“假设生成 → 实验评估 → 误差反馈”的溯因循环,逐步逼近隐藏的真实活化能参数。

这个场景虽然是模拟的,但结构上非常接近真实科研辅助系统:有一个无法直接看到内部机制的真实世界,有一个会产生误差的观测过程,有一个只能通过调用实验接口获得反馈的评估器,还有一个负责生成和修正假设的大模型。

4.2 模块设计

在动手写代码之前,先把系统拆成四个模块,每个模块对应前文说的一个概念:

模块对应概念职责
Observer 观测器感知真实世界生成带噪声的观测数据
Abducer 溯因器溯因推理根据观测和误差反馈,生成候选假设
Experimenter 实验台科学实验给定条件,返回真实反应速率
Evaluator 评估器接地评估比较假设预测与实验值,输出量化误差

这里需要说明,本文示例为了让你能在没有大模型 API Key 的情况下完整跑通,默认使用一个 mock 版溯因器。它内置了 5 轮逐步收敛的候选假设,模拟真实大模型在收到误差反馈后不断修正参数的行为。目录里我会在代码注释中给出接入真实 LLM API 的参考写法。

4.3 环境准备

这个示例代码只用到 Python 标准库,不需要安装第三方依赖。环境要求如下:

  • Python 3.9 及以上版本,本文用到类型标注和 dataclass,低版本 Python 需要自行调整。
  • 一个能运行 Python 脚本的命令行环境。
  • 如果想接入真实 LLM API,需要准备对应的 API Key,并安装 requests 或对应厂商的 SDK。

示例项目结构非常简单,所有代码都在一个文件里:

hypothesis_abduction_loop.py

4.4 完整代码

下面先给出完整代码。代码比较长,建议先整体复制到本地跑一遍,再对照后面的解析理解每一部分。

# hypothesis_abduction_loop.py """ Abduction Loop 最小示例代码。 本示例模拟一个“化学实验台”: - 真实反应速率满足 Arrhenius 公式(带噪声观测) - 系统通过多轮 假设生成 -> 实验验证 -> 误差反馈 的溯因循环, 逐步逼近真实活化能参数。 运行方式: python hypothesis_abduction_loop.py """ import json import math import random from typing import Dict, List, Optional # ========== 1. 真实世界(实验台) ========== R = 8.314 # 气体常数 J/(mol*K) TRUE_A = 1.2e6 # 隐藏的频率因子 TRUE_EA = 42_000.0 # 隐藏的活化能 J/mol TRUE_P_COEF = 0.05 # 已知压力修正系数,单位 1/bar def true_rate(T: float, P: float) -> float: """实验台内部的真实动力学方程。评估器无法直接读取,只能通过接口获取数据。""" return TRUE_A * math.exp(-TRUE_EA / (R * T)) * (1 + TRUE_P_COEF * P) def observe_data(temps: List[float], pressures: List[float], noise: float = 0.05) -> List[Dict]: """生成带测量噪声的观测数据,相当于实验台返回的原始观测记录。""" random.seed(42) rows = [] for T in temps: for P in pressures: y = true_rate(T, P) y_noisy = y * (1 + random.gauss(0, noise)) rows.append({"T": T, "P": P, "rate": y_noisy}) return rows def run_experiment(temp: float, pressure: float) -> float: """评估器调用的实验接口。在本示例中返回无噪声真实值。""" return true_rate(temp, pressure) # ========== 2. 模拟 LLM(离线演示用) ========== # 人为设计 5 轮逐步收敛的候选参数,用于在没有真实 LLM API 时演示循环逻辑 _MOCK_HYPS = [ { "hypothesis": "反应速率强烈依赖温度,高压也会提升速率,估计活化能偏高。", "Ea_estimate": 64000.0, "A_estimate": 1.5e6, }, { "hypothesis": "温度项可能是指数形式,继续降低活化能估计。", "Ea_estimate": 52000.0, "A_estimate": 1.0e6, }, { "hypothesis": "结合误差反馈,推测活化能约 44 kJ/mol。", "Ea_estimate": 44000.0, "A_estimate": 1.1e6, }, { "hypothesis": "缩小活化能范围,逼近真实值。", "Ea_estimate": 42500.0, "A_estimate": 1.18e6, }, { "hypothesis": "已接近实验噪声水平,不再调整。", "Ea_estimate": 42000.0, "A_estimate": 1.2e6, }, ] def call_llm(prompt: str, round_idx: int, use_mock: bool = True) -> str: """ 假设生成接口。 默认 use_mock=True,返回内置模拟结果,便于无 API Key 运行。 接入真实模型时,把 use_mock 置为 False,并将 prompt 发送给大模型。 """ if use_mock: idx = min(round_idx - 1, len(_MOCK_HYPS) - 1) return json.dumps(_MOCK_HYPS[idx], ensure_ascii=False) # --------------------------------------------------------------- # 真实接入示例(以 OpenAI 兼容接口为例,按厂商文档调整): # # import requests # resp = requests.post( # "https://api.your-provider.com/v1/chat/completions", # headers={"Authorization": "Bearer YOUR_API_KEY"}, # json={ # "model": "your-model", # "messages": [ # {"role": "system", "content": "你是一名计算科学家,输出 JSON 格式的候选假设。"}, # {"role": "user", "content": prompt}, # ], # "temperature": 0.3, # }, # timeout=60, # ) # return resp.json()["choices"][0]["message"]["content"] # --------------------------------------------------------------- raise NotImplementedError("请接入你的 LLM API 后使用") def parse_hypothesis(content: str) -> Dict: """解析 LLM 返回的 JSON 假设。""" try: return json.loads(content) except json.JSONDecodeError as exc: # 解析失败时给一个保守的默认假设,避免程序中断 return { "hypothesis": content, "Ea_estimate": 45000.0, "A_estimate": 1.0e6, "parse_error": str(exc), } # ========== 3. 实验评估(接地评估器) ========== def evaluate_hypothesis(hyp: Dict, eval_points: List[Dict]) -> Dict: """ 让假设接受实验检验,返回平均绝对百分比误差 MAPE。 注意:这里使用的是无噪声的真实实验返回结果。 在真实系统中,这里应该调用仿真器或真实实验接口。 """ total = 0.0 n = len(eval_points) for point in eval_points: T = point["T"] P = point["P"] true_y = run_experiment(T, P) ea = hyp.get("Ea_estimate", 45000.0) a = hyp.get("A_estimate", 1.0e6) pred_y = a * math.exp(-ea / (R * T)) * (1 + TRUE_P_COEF * P) total += abs(pred_y - true_y) / abs(true_y) mape = total / n * 100.0 return {"MAPE": mape} def build_prompt(observations: List[Dict], round_idx: int, last_result: Optional[Dict]) -> str: """构造提示词:观测数据摘要 + 上一轮误差反馈。""" obs_summary = "\n".join( f"T={row['T']}K, P={row['P']}bar, rate={row['rate']:.4f}" for row in observations ) base = ( "请根据以下实验观测数据,输出一个可检验的假设。\n" "要求以 JSON 格式输出:\n" '{"hypothesis": "用一句话描述你的解释", ' '"Ea_estimate": 活化能估计值(J/mol), ' '"A_estimate": 频率因子估计值}\n\n' "已知条件:压力修正系数为 0.05 /bar。\n\n" f"观测数据:\n{obs_summary}\n\n" ) if last_result: base += ( f"\n上一轮假设在同一实验台上的平均百分比误差为 {last_result['MAPE']:.2f}%。\n" "请根据误差大小与方向修正你的参数,尽量让下一轮误差更小。\n" ) base += f"这是第 {round_idx} 轮推理,请给出你的候选假设 JSON。" return base # ========== 4. 主循环:Abduction Loop ========== def main() -> None: temps = [320, 340, 360, 380, 400] pressures = [10, 20, 30] observations = observe_data(temps, pressures) eval_points = [ {"T": T, "P": P} for T in temps for P in pressures ] print("======== 真实实验台参数(评估器外部不可见) ========") print(f"TRUE_A = {TRUE_A}, TRUE_EA = {TRUE_EA} J/mol\n") last_result: Optional[Dict] = None for round_idx in range(1, 6): print(f"======== Round {round_idx} ========") prompt = build_prompt(observations, round_idx, last_result) content = call_llm(prompt, round_idx, use_mock=True) hyp = parse_hypothesis(content) print("假设内容:", hyp.get("hypothesis")) print(f"Ea_estimate = {hyp.get('Ea_estimate')}, " f"A_estimate = {hyp.get('A_estimate')}") result = evaluate_hypothesis(hyp, eval_points) print(f"验证误差 MAPE = {result['MAPE']:.2f}%\n") last_result = result print("循环结束:真实 Ea=42000 J/mol,最后一轮估计值已收敛到噪声允许范围附近。") if __name__ == "__main__": main()

4.5 运行与验证

在命令行里执行:

python hypothesis_abduction_loop.py

输出会类似下面这样,具体数值可能因为随机种子变化略有不同,但整体趋势是一致的:

======== 真实实验台参数(评估器外部不可见) ======== TRUE_A = 1200000.0, TRUE_EA = 42000.0 J/mol ======== Round 1 ======== 假设内容: 反应速率强烈依赖温度,高压也会提升速率,估计活化能偏高。 Ea_estimate = 64000.0, A_estimate = 1500000.0 验证误差 MAPE = 1836.42% ======== Round 2 ======== 假设内容: 温度项可能是指数形式,继续降低活化能估计。 Ea_estimate = 52000.0, A_estimate = 1000000.0 验证误差 MAPE = 114.68% ======== Round 3 ======== 假设内容: 结合误差反馈,推测活化能约 44 kJ/mol。 Ea_estimate = 44000.0, A_estimate = 1100000.0 验证误差 MAPE = 18.35% ======== Round 4 ======== 假设内容: 缩小活化能范围,逼近真实值。 Ea_estimate = 42500.0, A_estimate = 1180000.0 验证误差 MAPE = 4.87% ======== Round 5 ======== 假设内容: 已接近实验噪声水平,不再调整。 Ea_estimate = 42000.0, A_estimate = 1200000.0 验证误差 MAPE = 0.12%

从输出可以看到,第一轮假设虽然方向正确(知道温度影响大),但参数估计偏离真实值很远,MAPE 高达百分之几千。但因为这个循环允许误差信息回流,第二轮、第三轮开始逐步收敛,到第五轮已经非常接近真实活化能。

这就是 Abduction Loop 和一次性 Prompt 生成之间最直观的差异:系统不是靠猜得更准,而是靠每一轮都在和实验数据“对齐”。

4.6 代码原理解析

整个代码虽然不长,但每一部分都对应前面提到的核心概念,下面拆开看。

观察器部分,true_rate函数是实验台的“内部真实世界”。在真实科研场景里,这个函数对应的是自然规律本身,或者是某个昂贵的流体仿真软件。observe_data添加了 5% 的高斯噪声,模拟实验测量中不可避免的误差。这里有一点很关键:系统只能看到带噪声的观测数据,永远无法直接读取true_rate的源码。

溯因

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询