今天想系统性复盘一下我们团队最近半年做的一个项目:在对话式交互里处理气候模型数据,并且让系统具备“预测”和“解释”两种核心能力。内部代号我们叫它 OC 原型。
事情的背景是这样:我们手里有一批气候模型输出,体量大、维度多、格式复杂,团队里真正能熟练操作这些数据的人就两三个。业务方的同事天天拿着 Excel 跑来问数据,问的问题又特别重复——“某地区是不是越来越热了”“这个趋势显不显著”“历史上有没有类似年份”。一开始我们做了几个固定报表,后来发现根本不够用,因为同样的问题换个区域、换个时段,就得重新写脚本。
于是我们想试试另一条路:把自然语言对话作为前端,后端接一层工具化算子,让用户直接用中文提问,系统自动完成数据抽取、统计计算和结论生成。听起来挺简单,真正做起来完全不是一回事。尤其是标题里这两个词——“预测”和“解释”,放在气候数据场景下有完全不同于普通业务报表的含义。预测意味着你不能只做描述性统计,你得真正告诉用户未来可能发生什么,同时还得说清楚这个判断的依据和不确定性;解释意味着系统不能只甩一个数字,它得在对话里给出可追溯、可理解的逻辑链条。
这篇文章会整理我在这个项目里的完整思路、踩过的坑和最终沉淀下来的方案,适合三类人看:做气候或环境数据服务的开发者、想在自己的数据产品里接入对话能力的 AI 应用工程师,以及负责数据分析平台设计的产品和技术负责人。
1. 先想清楚:这个对话系统到底要解决什么
很多人一听到“对话式处理气候模型数据”,第一反应是“接个大模型,让它读数据然后回答”。我一开始也差点走这条路。后来答案被现实狠狠教训了一通:气候模型数据跟普通的文本、表格数据完全不一样,大模型直接读只会产出幻觉。
1.1 气候模型数据,不是简单表格
气候模型输出和业务数据库表是天壤之别。一个典型的气候模式输出文件,内部是四维网格数据——经纬度、高度层、时间步长,每个格点是一个物理量,比如地表气温、降水率、海表温度、土壤湿度。文件格式常用 NetCDF 或 GRIB,变量命名规则各模式体系各有一套,坐标系、单位、时间日历系统都不统一。
最头疼的问题是变量口径。同一个“降水”概念,在不同模式里可能是“总降水量”“对流性降水”“大尺度降水”,单位可能是毫米每天,也可能是千克每平方米每秒。你要做区域对比的时候,不先把这些口径统一掉,后面所有结论都是错的。
这就引出对话系统第一个关键设计原则:大模型不能直接触碰原始文件。所有数据读取、单位换算、坐标系对齐必须由独立的数据管道完成,对话层只负责把用户意图翻译成管道能执行的指令。
我类比一下:气候数据往小了说像一个超大仓库,里面放了成百上千张结构各异的网表,你问“全球平均温度十年变化趋势”,相当于问“那么多个子表,到底用哪一张、怎么聚合、按什么权重”,没有一条标准化的取数链路,任何人都没法直接回答。对话系统的第一层价值,是把这一整套取数链路固化成可复用的工具。
1.2 预测和解释,是两件不同的事
项目标题特别准确地点出了两个核心能力。但在动手之前,必须把这两件事彻底拆开,否则项目会做成一个四不像。
我先说预测。在气候数据语境里,“预测”不是大模型想象出来的未来,而是基于历史数据或模式输出,借助统计方法或物理模型做出的推断。具体可以分成几类:趋势外推(过去三十年持续升温,未来十年维持趋势的概率有多大)、极端事件重现期(五十年一遇的高温阈值是多少)、多模式集合统计(新一代气候模式里,有多少个模式支持本地区夏季降水增多)。
再说解释。解释包括四个层次:数据来源(这个数字来自哪个数据集、哪个版本、哪个模式)、计算方法(用了什么统计检验、什么置信水平)、不确定性表达(置信区间是多宽、样本量有多小)、对比基准(和气候态比、和上一个十年比、和其他模式比差多少)。
如果把预测和解释混为一谈,系统就会给出一种特别糟糕的答案:看起来像专家,实际上说不清任何依据。
所以在架构设计上,我从第一天就定了一个原则:预测能力由统计算子提供,解释能力由渲染层围绕算子结果生成。两者严格分离,谁都不能跨界。
1.3 为什么我不用微调,而是走工具调用
这是整个项目最早做的技术决策。当时团队内部争论过:要不要收集一批气候问答对,微调一个专用模型,让它直接回答所有问题。我们后来全体否决了这条路线,理由非常实际。
第一是数据时效性。气候数据范围在不断扩大,新版本模式持续发布,你微调一次模型需要几周时间,等模型上线数据早就过时了。第二是计算可复现。科研评审或业务决策都要求“这个数字是哪里来的”,你让大模型直接生成数字,等于把计算过程变成了黑箱,谁都没法复核。第三是幻觉问题。模型只要有一次胡编,用户就不再信任整个系统,而气候数据领域用户几乎都是专业从业者,被抓住一次编数,项目基本就废了。
工具调用(function calling)的思路完全不同:让大模型只扮演调度员和翻译官的角色。用户的自然语言进来,模型负责识别意图、抽取参数、决定调用哪个数据算子,算子返回结构化结果,模型再把结果翻译成自然语言。计算这个环节,完全在可信的数值环境里完成。
我和团队复盘时列过一个简单对比:
| 维度 | 专用微调模型 | 工具调用 + 通用模型 |
|---|---|---|
| 数据更新成本 | 每次重新训练 | 只更新后端算子 |
| 数值准确率 | 不稳定,容易幻觉 | 全部由算子保证 |
| 可解释性 | 较低 | 每个结论可溯源 |
| 对话流畅度 | 高 | 高 |
| 开发周期 | 数据处理 + 训练,周期长 | 算子开发 + 接口封装,周期短 |
这个对比基本决定了我们的技术路线。后面所有工作,都是围绕“模型不产生任何数值”这个铁律展开的。
2. 预测能力是怎么一步一步造出来的
讲完整体思路,进入实现层面。预测能力是整个系统中专业含量最高的模块,也是用户最看重、同时又最容易翻车的部分。
2.1 先搭一条干净的数据管道
任何预测结论都不可能在脏数据上建立,所以数据管道是整个系统最枯燥但最关键的底座。我们花在这个环节的时间占了整个项目的一大半。
数据管道的核心任务包括:将各类气候模式输出统一转换成内部标准格式;把不同模式的网格重采样到统一分辨率;把时间轴统一到公历,处理 365 天和 360 天日历的差异;对缺失格点做插值,并对极端异常值做标记。我现在截图给你看我们内部的标准数据结构,每个变量除了数值,还带着单位、网格描述、时间范围、来源标记。
数据管道的产物是一套统一的“数据访问层”。业务侧同学不用关心原始文件长什么样,只需要通过参数调用:给定变量名、区域范围、时间段、聚合方式,返回标准化结果。
代码层面我们主要用 Python 生态,读写用 NetCDF 处理库,运算用 NumPy 和 SciPy,并行处理走 Dask。这里有一个非常实用的经验:不要试图把所有场景的预处理逻辑都写在一个大函数里,一定要按变量类型拆分处理模块。比如气温类变量只需要做高度层可选聚合,降水类变量必须额外处理对流和大尺度的区分,海温变量则要处理掩膜和海岸线问题。
2.2 把“预测”拆成可调用的算子
数据管道就绪后,下一步是把预测能力封装成一个一个可以独立调用的算子。这样对话层每次预测请求,本质上都是组合若干算子的结果。
我们第一批上线的算子有五个:
第一,趋势分析算子:输入区域、变量和时间段,输出线性趋势斜率、显著性水平、置信区间。实现上用了非参数方法,对气候数据这种有异常值的场景稳定性更好。
第二,气候态异常算子:计算某时段相对基准期的偏离程度,输出距平和距平百分率。
第三,分位数与极值算子:求解指定时间段内变量的百分位数,以及超过阈值的天数或事件频次。
第四,重现期估计算子:基于历史序列,用广义极值分布拟合,估算不同重现期对应的强度阈值。
第五,多模式集合统计算子:把多个气候模式的同类变量做集合统计,输出集合中位数、四分位区间和模式间一致率。
每个算子都严格定义输入和输出。以趋势分析算子为例,输入是一个标准化的时间序列,输出是一个 JSON 对象,包含斜率、截距、P 值、置信区间上下界、样本量,以及“趋势方向判定”字段。这个输出结构会被后续渲染层直接使用。
有一个细节值得单独说:气候数据的趋势检验不能直接用普通线性回归的 P 值,因为时间序列往往自带自相关。自相关性会让有效样本量虚高,从而低估 P 值,得出“显著”的错误结论。我们在算子里预置了去自相关修正的逻辑,这一步虽然增加了计算量,但从后续用户反馈看,专业用户对这个细节的认可度极高。
2.3 一次完整的趋势预测:从提问到结论
把算子准备好之后,需要走通一条完整的链路。我拿一个常见问题来演示:“帮我看看目标流域近三十年夏季降水的趋势。”
用户在对话框发来这句话后,对话调度层做四件事:
第一,意图识别。系统判定这是一个趋势分析类请求,命中“趋势分析算子”。
第二,参数抽取。需要从这句话里拿到三个关键参数:区域“目标流域”、变量“降水”、时段“近三十年”。这里的难点是“近三十年”是模糊表达,需要解析成具体的起止年份,并确定“截止到当前数据的最新可用年份”。
第三,工具路由与执行。调度层把参数交给趋势分析算子执行。后端会先调用数据访问层,取出该流域逐年夏季区域平均降水序列,然后执行趋势检验,得到斜率、P 值等结果。
第四,结果渲染。渲染层拿到算子的结构化返回,生成一段有依据的自然语言答案。
后端算子的简化逻辑类似下面这样:
import xarray as xr import numpy as np from scipy import stats def load_series(variable, region, season, period): # 省略数据读取和区域聚合细节 # 返回一个一维时间序列 pass def trend_analysis(ts): # 这里用一个稳健的斜率估计,替代普通最小二乘 # 先对序列做自相关修正,再计算显著性 n = len(ts) idx = np.arange(n) slope, intercept = stats.theilslopes(ts, idx)[:2] # 自相关修正后重新计算标准误和 P 值 acf = np.corrcoef(ts[:-1], ts[1:])[0, 1] n_eff = n * (1 - acf) / (1 + acf) # 基于 n_eff 重新计算置信区间 # 返回结构化结果 return { "slope": float(slope), "unit": "mm/decade", "p_value_adj": 0.03, "ci_low": 8.2, "ci_high": 14.5, "direction": "increasing", "significant": True }实际对话里,用户最后看到的是:
“目标流域近三十年夏季降水呈增加趋势,速率为每十年增加约 11 毫米,置信区间为 8.2 至 14.5 毫米,经自相关修正后趋势在 95% 置信水平上显著。该结论基于覆盖 20 个模式的集合平均结果,作为参考,上一十年速率为每十年 6 毫米。”
这段回答里的每一个数字,都能在前面算子的返回结果里一一对应。
3. 解释能力怎么落地
如果说预测能力决定系统值不值得用,那解释能力直接决定用户信不信你。
3.1 科学结论不能说“我觉得”
对话系统最大的风险,是用户把系统当成一个“权威专家”。一旦系统说出一句没有任何依据的结论,而且语气很确定,影响会很差,尤其是气候数据这种要支撑决策的领域。
我们的应对策略非常硬:任何最终回答,只能引用工具调用返回结果中明确存在的字段。对话编排层负责把所有工具返回的结果注入上下文,但在注入前,会过滤掉与当前问题无关的字段,以防止模型自己去“发挥”。我们在指导思想里强调,工具返回结果是一种受控证据,模型只能引用,不能想象。
举一个反例,早期版本出现过这种情况——用户问某地区未来是否更干旱,系统回答“根据数据分析,未来干旱风险显著增加”。这句话本身逻辑没问题,但当时算子返回的只是 SSP 情景下的降水变化中位数,并没有做过干旱定义检验。“干旱风险”这个概念是模型自己引申出来的。后来我们给所有回答加了严格的“概念边界”规则,算子没算过的东西,回答里不允许主动出现。
3.2 对话里解释的四个层次
为了让解释既专业又不过度冗长,我们把解释信息按层次拆成四层。渲染时会根据用户意图决定展示哪几层。
第一层是数据来源层。它的作用是让用户知道结论基于什么。表达方式类似:“该结论基于 20 个全球气候模式的集合输出,时间段为 1995 至 2024 年。”
第二层是计算方法层。它的作用是让专业用户可以复核方法是否合理。表达方式类似:“趋势检验采用去自相关修正的稳健估计,置信水平为 95%。”
第三层是不确定性层。它的作用是让用户了解判断的可靠程度。表达方式类似:“各模式间一致性较弱,集合区间跨度较大,该结论的确定性为中等。”
第四层是对比基准层。它的作用是帮助用户判断具体程度。表达方式类似:“相较基准期,当前趋势速率提升约 80%。”
这四层不是永远同时出现。默认答案只包含第一层和第三层的部分信息,如果用户追问“这个结论靠谱吗”,我们再展开计算方法和置信区间的内容。如果用户追问“和以前比怎么样”,再补充对比层。
如果目标是让系统更接近真人专家,这套分层逻辑确实管用。真人专家的习惯也是先说结论,被问到才展开依据。
3.3 解释信息太多怎么办:分层呈现
但这里马上带来一个新问题:如果每次回答都把这四层信息全部塞进去,对话会变得极其冗长,用户基本看不完。
我做过一次小规模体验测试,同一段结论分别用 3 行、6 行、10 行呈现,测试者完成“提取关键结论”任务的速度差异极其明显。3 行版本几乎零思考就能读完,10 行版本大量测试者读到最后已经不记得开头的结论。
所以我们定了一个默认的渲染模板,结构是“一句话结论 + 一句核心依据 + 一句代表性数据说明”,总长度控制在 100 字以内。用户如果追问“为什么”“怎么算的”“可不可信”,再触发完整版本。
举个例子。默认回答是:“目标区域未来二十年夏季降水概率增加,集合中位数比基准期增加 9%,解释量约 70%。”这里已经包含结论、数字和不确定程度。
追问后才展开成完整版,补充模式数量、统计方法、置信区间、各情景对比。这种渐进式披露的策略,既避免了信息过载,又保留了系统“出结论时给足依据”的专业形象。
4. 实操过程:最小可运行系统怎么拼起来
前面的章节讲的是设计逻辑,这一节直接说实现。目标是让你看完之后能照着一个最小可运行的骨架去搭自己的系统。
4.1 技术栈与模块划分
我们的技术栈属于非常主流的组合。对话层使用支持工具调用的大模型接口,后端是 Python 服务,算法层用 NetCDF 处理库、NumPy、SciPy,数据存储采用列式文件格式加速读取。整体分四个模块:
数据访问模块负责连接底层数据,提供格式统一的时间序列或网格数据。算子模块封装所有统计计算,输出标准化 JSON。对话编排模块负责意图理解、参数抽取、工具路由和结果注入。渲染模块负责把 JSON 翻译成自然语言。
模块划分有一条红线:模块之间只能通过参数和 JSON 结果交互,禁止业务数据的深度耦合。比如对话编排层永远不直接读文件,所有数据都从算子模块拿现成结果。这样做的最大好处是系统可以独立测试——算子模块可以单独跑单元测试,渲染模块可以对着固定 JSON 结果做回归测试。
架构的关键链路是:用户提问 → 对话模型输出“要调用什么工具以及参数” → 后端执行 → 返回值注入上下文 → 对话模型生成最终回答。
4.2 工具接口与参数约束
工具调用能不能稳定工作,很大程度上取决于接口定义得够不够严。宽泛的接口会让模型自由发挥,产生各种边界外的参数。
每个工具的 JSON Schema 都要做到参数类型明确、枚举值穷举、可空字段越少越好。我拿趋势分析算子举例:
{ "name": "trend_analysis", "description": "对指定区域、变量、时段执行趋势分析,返回斜率、显著性和置信区间。", "parameters": { "type": "object", "properties": { "variable": { "type": "string", "enum": ["tas", "pr", "tos", "psl"] }, "region": { "type": "string", "enum": ["target_basin", "coastal_zone_a", "inland_zone_b"] }, "period": { "type": "object", "properties": { "start_year": {"type": "integer"}, "end_year": {"type": "integer"} }, "required": ["start_year", "end_year"] }, "season": { "type": "string", "enum": ["DJF", "MAM", "JJA", "SON", "annual"] } }, "required": ["variable", "region", "period", "season"] } }为什么枚举值要写死?因为气候数据里的变量名非常混乱。你把“pr”提供给模型时它能正确映射,但如果你让模型自己选择“降水”“降雨”“rainfall”“precipitation”,它反而花更多时间纠结,还容易选错。我们把取值范围限定到内部标准代码,并明确写清楚描述,模型就能稳定命中。
所有参数还必须经过一次本地校验。即使模型给出了不符合枚举值的参数,后端也不会直接执行,而是返回一个参数错误,由对话层发起追问。这个校验看似麻烦,实际能拦截大量诡异问题。
4.3 性能与成本优化实录
气候数据天然体量大,如果用户每问一个问题,系统都需要临时读一遍几 GB 的文件,体验会直接崩溃。我们花了很多精力做性能优化,沉淀下来三个最有效的手段。
第一是预聚合。我们把用户最高频的查询组合——常见区域、常见季节、常见变量——离线算好底表。用户问的知识如果命中底表,直接读小文件,十毫秒内返回;没命中的才走实时计算。预聚合带来的收益远超预期,内存占用和响应时间都大幅下降。
第二是降分辨率策略。有些问题根本不需要原分辨率数据。比如用户问“全国平均气温趋势”,这时用 2 度分辨率的网格就可以了,不需要用原始 25 公里网格。我们根据查询精度需求设置了多档聚合策略,在不损失结论质量的前提下大幅压缩计算量。
第三是上下文长度控制。工具返回结果不能全部塞给模型。趋势分析有完整的时间序列,如果统统把几十个数值丢进上下文,既浪费 token,又干扰模型注意力。我们在工具内部就完成摘要化,返回结果只包含统计指标和必要的展示数据,序列只保留末位数个点。配合截断策略,上下文长度从几千 token 降到了几百 token。
5. 常见问题与排查实录
这段记录我们在实际运行中遇到的最典型的四类问题。每一条都是真金白银换来的经验。
5.1 大模型开始一本正经地编数据
这是最危险的故障。早期版本里,我们渲染层的 Prompt 写得过于宽松,模型在生成答案时偶尔会在结论中混入一些补充的表情或判断,比如“数据显示,该地区夏季降水有显著上升趋势,且上升幅度创历史新高。”问题在于,算子并没有返回“创历史新高”的信息,这句话是模型听上去合理但实际并不存在的推断。
我们的排查方式非常清楚:开启完整链路日志,记录每一次工具调用的输入输出和模型的最终回答,然后逐字比对。一比对就发现,凡是出现编造数字或编造结论的回答,共同特征都是模型在试图补充算子结果之外的“额外解释”。
这事的根治办法是双重的。Prompt 层明确告诉模型,不得在工具结果之外新增任何数据性表述;同时渲染层的校验脚本检查最终回答中出现的数字是否全部能被算子结果覆盖。数字级别的校验就像下了双重保险,基本堵死了编数的问题。
5.2 “最近十年”“这一带”这类模糊表达
气候数据对话里,用户几乎不会精确地说“请计算 2014 到 2023 年”,他们只会说“最近十年”。区域表达更模糊,“这一带”“该地区”“我们这边”都需要系统处理。
我们处理模糊时间靠两个机制。第一是规则解析,把“最近 N 年”“过去 N 年”“上世纪”这类表达转换成具体的起止年份,并且根据当前数据的时间上限确定终点。第二是澄清机制,如果系统对时间范围的解读不够确信,就会追问一句:“您说的最近十年,是按当前数据最新年份往前推十年吗?”防止默认猜错方向。
区域名处理更讲究,涉及地名词典。我们把常用地理概念映射到一个内部统一的地理区域编码。用户说“某流域”,系统解析到该流域的经纬度多边形。如果出现多个同名区域或者区域太宽泛,同样采用澄清策略。这里有一个明确的避坑经验:不要让模型自己猜测区域边界。区域边界错误会导致整个运算全错,而模型最不擅长处理经纬度多边形这类精确坐标。
5.3 回答太长,用户直接放弃
有一类问题在产品上没什么技术含量,但影响却很大:系统生成的回答过于长篇大论。这种情况在早期频繁出现,一旦用户问“给我详细说说这个趋势”,模型会把计算背景、方法、数据来源、未来展望全写进回答里,长度直逼一篇论文摘要。
用户面对长回答往往只会读前两行,然后放弃追问。而且长回答会让模型更容易产生幻觉,因为生成的 token 越多,跑偏概率越高。
我们的解决措施是渲染模板化。趋势类问题强制使用“结论 + 依据 + 不确定性”三句式模板;相关关系类问题使用“相关性数值 + 样本量 + 显著性”模板;情景对比类则用“中位数 + 区间 + 模式一致性”模板。任何回答超出模板结构的,渲染层自动截断。
5.4 用户把概率预测当成确定结论
气候预测和天气预报有本质区别。天气预报说“明天有雨”,大约一个比较可靠的结论;气候预测说“本世纪中叶降水可能增加”,是一个概率性的判断。普通用户很容易把后者当成“确定会这样”,从而做出偏激的决策。
我们在努力解决这个问题。渲染层在输出预测结论时,强制要求附带不确定度量,可以用置信区间数字,也可以用“确定性中等”“模式一致性较弱”这个等级标识。对于极端事件类预测,还会特别强调“这不是确定性预报,而是基于历史统计的模式推断”。
有一次用户问“未来是否会出现更强的台风”,系统给了一个强度范围的概率值,并注明“该推断依赖于未来的排放情景”。用户一开始不满意,觉得系统不够干脆。但后续的使用中,他和团队沟通反而更顺畅了,因为能清楚知道结论的条件和边界。这提醒我:用户要的往往不是确定性,而是可决策的信息边界。
6. 如果再给我半年,我会调整这些
项目做到后面,沉淀了不少对方向的重新思考。这部分不是客套的总结,而是我真实会在下一期项目里改变的东西。
6.1 能力边界要先定,可解释永远优先
我们第一期项目有点冒进,过早放上了重现期估计和极端事件算子。结果就是,这些算子边界条件复杂,但系统的解释与校验还没跟上,产出过一批“看起来高大上、实际上存疑”的答案。
下一期我会把顺序倒过来:先做统计描述和趋势推断,把解释模块打磨得再扎实一些,再上预测类和极值类工具。能力边界必须在系统上线前明确定义,什么场景做预测、什么场景只做统计描述,写清楚。这个边界可以防止系统被用户带去它不该去的地方。
6.2 自动化评测集一定要早做
对话系统最坑的一点是人工测试耗时间,而且每次改 Prompt 后都得重新手工试一遍。我们第一期的评测基本靠手测,效率很低,而且不同人的评测标准不一致,有的人看数值准确,有的人看语气专业,结论没法统一。
下一期一开始就要建一个自动化评测集。我的设计思路是这样的:准备一百个覆盖全部算子类型的高频问题,每个问题预置正确答案模板和关键数字;评测时运行完整链路,自动提取回答中的关键数字与预置答案对比,并检查强制引用字段是否真实存在。这样的自动化评测可以每天的构建任务里跑一遍,确保每一次改动都没有破坏核心能力。
6.3 后面还能扩展的玩法
再往后,还可以根据这个架构扩展三个方向。第一个是自动化报告生成:在对话结论的基础上,自动组合成结构化文字报告,附带图表链接,适合周期性数据研判。第二个是异常预警:用户预设阈值,系统定期扫描数据,一旦超出阈值自动推送消息,对话层负责解释预警的统计含义。第三个是多模式一致性可视化:不再只给一个集合中位数,而是把各模式倾向分布以分布图形式呈现,配上系统生成的解释语言。
这三个扩展方向都建立在“工具调用 + 算子后端 + 分层解释”这个基础上,不需要推翻现有架构。
最后说说我的个人体会。这半年做下来,我最大的感受是:在气候数据这种专业领域,对话系统真正稀缺的不是华丽的话术,而是克制。系统必须知道自己能回答什么、不能回答什么,并且永远能被用户追问下去。能支撑起追问的,不是更大的模型参数,而是后端那些计算逻辑严谨、数据来源可溯的算子。这套逻辑,其实在任何数据型对话产品里都通用。