1. 问题拆解:慢病场景下,大模型幻觉为什么这么致命
先说结论:慢病AI陪伴系统,和通用聊天机器人最大的区别在于,它犯错的代价完全不是一个量级。普通问答答错了,用户笑一笑就过去了;慢病场景里,一个饮食建议、一个用药提醒、一次血糖解读,错了就是实实在在的健康风险。
我接触过不少做AI医疗产品的团队,大家最初的想法都非常一致:把大模型接进来,把慢病指南、药品说明书、临床路径喂进去,让模型变成一个“什么都知道”的健康管家。这个思路本身没错,但落地之后问题就来了。
慢病管理有三件事是通用大模型天然做不好的。
第一,数值计算类问题。比如“我身高170,体重72,帮我算一下BMI”,模型可能给你算出一个27.3,也可能给你算出一个26.8——都不是瞎算,但也不是同一个数。问题在于,大模型本质上是根据token的概率分布来“猜”答案的,它不是在执行计算,它是在模仿人类回答计算的语气。再比如“我今天摄入的碳水大概是多少”“我这几天的血糖波动系数是多少”,这类精确计算任务,让模型自由发挥就是在碰运气。
第二,需要长期动态跟踪的问题。慢病管理是一个连续过程,用户上周的空腹血糖、这周的餐后血糖、运动和饮食的依从性,这些数据之间有关系。但大模型每一次回复时,它能看到的是当前对话的上下文和知识库,它没有真正的长期记忆,更不会对数据进行跨时间段的统计和建模。你问它“我这一周的血糖控制怎么样”,你要是给它一堆历史数据,它也能说出个大概,但如果你想让它基于累计数据主动预警、动态调整方案,它很难靠“聊”来完成。
第三,对错误信息的“自信”问题。这其实是大模型最折磨人的一点:它给出错误答案的时候,语气和给出正确答案的时候一模一样。在慢病场景里,这种没有根据的自信是最危险的,因为它会诱导用户产生信任感。用户不一定会去核对AI给的运动建议是否合理,他可能真的照着做了。
所以纯靠大模型做慢病陪伴,本质上是在用一个“会聊天的模糊推理机”去干“需要精确计算和持续跟踪”的活。这个错配就是幻觉问题的根源。
我个人的判断是,慢病AI的系统架构,必须从“让模型背知识、讲道理”转向“让模型做交互、做表达,把计算和推理交给确定性的模块”。这就是这篇文章要讲的思路:用可计算数学模块去兜底一切可计算的环节,把大模型的发挥空间严格约束在理解和生成这两件事上。
1.1 为什么慢病场景对确定性有极高要求
慢病管理的核心特征是“长期”和“量化”。一个糖尿病患者需要管理的是几十年的日常生活,每一次饮食、运动、用药都可能是变量。这个场景要求系统给出的建议是可追溯、可复现的:今天BMI是25.1,明天也必须算出25.1;同一份血糖记录输入进去,出的波动系数必须完全一致。
人类医生能做到这一点,是因为他依赖的是公式、指南和检查数据。大模型靠的是参数空间里的记忆,它记下了“BMI=体重/身高/身高”这个公式,但它不会真的去计算,它是在生成一段大概率正确的算式和答案。一次两次可能对,一旦输入的数值特殊一点——比如身高是158厘米不是1.58米,体重是“大约70公斤”——模型就可能在单位换算、整数取整这些细节上出错。
可计算数学模块的价值就在这里:它把“公式”从模型的大脑里搬到了代码里。公式不是被“记住”的,而是被“执行”的。输入什么、输出什么、中间经过哪些步骤,每一环都是确定性的。这从根本上消除了计算类幻觉。
1.2 幻觉问题的本质:语言模型能做的事和不能做的事
为了把解决方案讲清楚,我们得先明确边界。大模型擅长的是:语义理解、意图识别、多轮对话、知识归纳、文本生成。这些能力来自大规模预训练,本质上是模式匹配和概率采样。
大模型不擅长的是:精确数值计算、逻辑推理的链式执行、跨时段数据的状态维护、对事实一致性的约束。这些不是“做得不够好”,而是架构上就不适合——它在生成答案时根本没有“算”这个动作,它的每一步都是从一个概率分布里采样。
明白了这一层,系统的设计原则就出来了:凡是能用确定性方法解决的,就不要让模型做;凡是模型做不了的,就不要让模型做。
比如说:
- 算BMI、算BMR、算TDEE、算血糖波动系数——交给可计算模块。
- 判断用户当前的血糖值是否超出阈值、是否触发预警——交给规则引擎。
- 判断用户说这句话背后的情绪和意图、组织一段有温度的话来叮嘱用户——交给大模型。
边界划清楚了,系统的可靠性上限就完全不一样了。
2. 方案选型:可计算数学模块的架构定位
可计算数学模块听起来高大上,实际上可以理解为一套能够被大模型调用的、确定性好的计算函数库。它有四种常见的存在形态,我接触过的项目里基本都可以归到这几类:
- 纯Python/Rust函数库,通过Function Calling机制被模型调用。
- 基于NumPy/Pandas的数据分析管道,负责处理批量数据和生成统计指标。
- 独立的规则引擎或计算服务,通过API对外提供能力。
- 符号计算与方程组求解模块(用于更复杂的给药建议、营养配比优化)。
这篇文章主要讲前两种,因为它们覆盖了慢病AI陪伴系统里80%以上的计算需求,而且实施成本最低——你不用引入额外的复杂基础设施,在现有代码库里加一层工具层就行。
2.1 可计算模块的三边界原则
在设计这个模块时,我总结了一个“三边界原则”,你可以直接拿去做架构评审的检查清单。
边界一:状态存储归模块,记忆归数据库。大模型本身不保存用户状态。用户所有的历史生理数据、依从性记录、画像标签,都应该存在独立的数据库里。计算模块直接从数据库取数,算完结果回写数据库。模型只负责在单轮对话里接收计算结果,然后用自然语言表达出来。
边界二:决策建议归规则和公式,话术生成归大模型。这个很重要。比如“今天要不要增加运动量”这个决策,不应该让模型根据感觉回答,而应该由规则引擎根据用户最近三天的血糖趋势、运动记录、当前状态来算出结论,然后把这个结论交给模型去“润色”成一句人话。决策和表达分离,是慢病AI系统逻辑洁癖的核心。
边界三:异常数据拦截归计算层,模型永远不许对错误数据将错就错。用户输入的数据可能本身不合理。比如血糖值输入了“58”,正常人空腹血糖根本不可能到这个数。这个判断要在数据进入计算层之前就完成,由计算层直接抛出异常信息给模型,让模型引导用户重新输入。模型没有权限对非法数据做出任何“基于用户输入数据”的健康解读。
这三条边界不是凭空想出来的,是我从好几个实际项目里踩坑踩出来的。早期我们让模型直接解析用户输入、直接算结果、直接给建议,结果就是系统里到处都是被模型“想当然”处理掉的异常值和错误结论。
2.2 为什么选择Python作为计算模块基础语言
慢病AI系统的计算模块,我首推Python。原因有三个。
第一,生态匹配。慢病相关的计算,无论是统计分析、时间序列平滑、还是未来的营养优化、风险预测,Python的SciPy、Pandas、scikit-learn生态是现成的。你不需要为了一个计算模块去引入另一套技术栈,团队维护成本低。
第二,和大模型的集成就很顺畅。现在主流的Agent框架——不管是OpenAI Function Calling、还是国内的百度千帆、通义百炼——都支持把Python函数直接注册成工具。你写好一个函数,一个@tool装饰器挂上去,模型就知道“有这么个工具可以用”,调用约定直接通过JSON Schema描述。数据格式都是JSON,两边的亲和度很高。
第三,逻辑可测试性好。计算模块是确定性代码,你完全可以写单元测试来验证每个函数的输入输出是否正确。这在医疗场景里是刚需——你不能上线一个连算数都能算错的模块,而Python标准的unittest/pytest体系和CI流程,能保证每次修改都不破坏原有逻辑。
2.3 模块与知识库的关系:计算归计算,知识归知识
还有一个方案选型问题很容易被忽略:可计算模块和大模型的知识库,到底是什么关系?
我的建议是:知识库管“宽泛的知识”,计算模块管“精确的推导”。
知识库里存的是慢病指南、饮食宜忌、用药常识、常见症状的科普解释。这些内容是相对静态的,大模型可以基于这些内容做语义层面的回答和解释。但是一旦用户的追问涉及到“根据我昨天的空腹血糖6.1和今天早上运动后的5.4,我为今天的午餐应该做怎样的碳水调整”,知识库就管不了了——这个问题不是靠查资料能回答的,它需要基于用户个体数据做计算推导。
所以知识库负责给模型提供“背景知识”,计算模块负责给模型提供“个体化结论”。模型把二者组合成最终话术。这种分工,可以让系统在保持专业知识宽度的同时,又保证个体化建议的准确度。
3. 核心计算函数设计与实现
现在进入正题。我把慢病AI陪伴系统里最常见、最高频的核心计算函数拆出来,逐个讲清楚设计思路和实现代码。你完全可以照着这套代码去搭自己的模块。
3.1 生理指标计算:BMI、BMR、TDEE
这三个指标是慢病管理的基石,很多具体的饮食和运动建议都要依赖它们。它们的公式都是公开的标准公式,但落地时有一些细节要处理好。
# core_metrics.py """ 慢病AI陪伴系统 - 生理指标计算模块 标准公式:BMI / Mifflin-St Jeor BMR / 基于活动系数的TDEE """ def calculate_bmi(weight_kg: float, height_cm: float) -> dict: """ 计算BMI(身体质量指数) Args: weight_kg: 体重(公斤) height_cm: 身高(厘米) Returns: 包含BMI值和分级的字典 """ # 单位转换:厘米 -> 米 height_m = height_cm / 100.0 # 边界检查:拒绝明显不合理的输入 if not (20 <= height_cm <= 250): raise ValueError(f"身高数值异常: {height_cm}cm,请确认输入是否正确") if not (20 <= weight_kg <= 500): raise ValueError(f"体重数值异常: {weight_kg}kg,请确认输入是否正确") bmi = round(weight_kg / (height_m * height_m), 1) # BMI分级(中国成人标准) if bmi < 18.5: level = "偏瘦" elif bmi < 24.0: level = "正常" elif bmi < 28.0: level = "超重" else: level = "肥胖" return { "bmi": bmi, "level": level, "advice": f"您的BMI为{bmi},属于{level}范围。" }这里有个小细节值得展开说明:单位问题。用户报身高的时候喜欢说“一米七”“170”“1米70”,体重喜欢说“140斤”“70公斤”。早期系统踩过坑:模型把“140斤”当成140千克去算BMI,结果算出个离谱的“肥胖”。后来我们在计算函数里统一做了一步:在进入函数前,先用一个归一化函数把中文描述转换成标准单位数值。这个转换逻辑一定要放在调用计算函数之前,否则再精确的公式也架不住脏输入。
BMR的计算方法,目前医学界比较认可的是Mifflin-St Jeor公式,它比传统的Harris-Benedict公式在普通人群中的准确度更高。
def calculate_bmr(weight_kg: float, height_cm: float, age: int, sex: str) -> float: """ 使用Mifflin-St Jeor方程计算基础代谢率(BMR) BMR(男) = 10 * 体重(kg) + 6.25 * 身高(cm) - 5 * 年龄 + 5 BMR(女) = 10 * 体重(kg) + 6.25 * 身高(cm) - 5 * 年龄 - 161 """ if sex not in ("male", "female"): raise ValueError("sex参数必须为 'male' 或 'female'") if sex == "male": bmr = 10 * weight_kg + 6.25 * height_cm - 5 * age + 5 else: bmr = 10 * weight_kg + 6.25 * height_cm - 5 * age - 161 return round(bmr, 1)TDEE的计算就是在BMR的基础上乘以“活动系数”,这个系数一般有五个档位:久坐(1.2)、轻度活动(1.375)、中度活动(1.55)、高度活动(1.725)、极高强度活动(1.9)。做慢病管理,我一般建议在给用户建议摄入热量时,不要用最高的两个档位,因为慢病用户的日常活动量大部分集中在轻度到中度之间。用高了容易把热量目标调高,不利于体重控制。
3.2 血糖波动评估:不只是看单点数值
很多初做慢病陪伴的产品都会犯一个同样的错误:只盯着用户的“当前血糖值”,高于某个阈值就预警,低于某个阈值就提醒加餐。但实际上,单点血糖值的意义远不如血糖波动趋势重要。一个血糖平稳但略高的人,和一个血糖大起大落但平均值正常的人,后者的并发症风险反而更高。
所以计算模块里必须有一个用来评估血糖波动情况的函数。医学界常用的指标叫“血糖变异系数(CV)”,计算公式是:血糖标准差 / 平均血糖,正常参考值一般建议在36%以下。
def calculate_glucose_variability(glucose_records: list) -> dict: """ 计算血糖波动评估指标 Args: glucose_records: 血糖记录列表,每个元素为 {"value": 5.6, "time": "2025-01-01 07:30"} Returns: 包含平均值、标准差、变异系数(CV)、最高最低值等指标 """ if not glucose_records or len(glucose_records) < 3: return { "status": "insufficient_data", "message": "至少需要3条血糖记录才能计算波动指标" } values = [record["value"] for record in glucose_records] mean = sum(values) / len(values) # 注意:这里应该用样本标准差(n-1),不是总体标准差(n) variance = sum((v - mean) ** 2 for v in values) / (len(values) - 1) std_dev = variance ** 0.5 cv = (std_dev / mean) * 100 if mean != 0 else 0 if cv < 20: trend = "血糖波动较小,控制良好" elif cv < 36: trend = "血糖波动在可接受范围内,建议继续保持" else: trend = "血糖波动偏大,建议关注饮食规律和用药依从性" return { "mean_glucose": round(mean, 1), "std_dev": round(std_dev, 1), "cv": round(cv, 1), "max_glucose": max(values), "min_glucose": min(values), "trend": trend }有一段时间我们只用了内置的statistics.stdev函数,后来发现这个函数在数据量特别小的时候结果波动很大,但用病区真实血糖记录做过验证后发现,当样本量少于3条时,任何统计指标都没有意义,所以直接在函数里加了最小样本量检查。这条经验想分享给各位:计算模块要有自己的“拒绝服务”逻辑,不要什么数据都接,接了就算。
3.3 饮食摄入估算与营养配比
慢病AI陪伴系统里用户问得最多的一类问题是:“我今天吃了XXX,大约摄入多少热量?”这个问题看似简单,实则容易踩坑。
我的做法是构造一个食物营养数据库(本地JSON或SQLite),存常见食物的每100g热量、蛋白质、脂肪、碳水含量。计算模块按食物名和估算重量去查表,然后加权汇总。这里的关键不是数据库本身,而是用户数据输入的不确定性。用户说“吃了一碗米饭”,到底是多少克?这个时候不能靠模块猜,而是要在对话里让模型和用户澄清:“这一碗米饭大概是你平时吃饭的小碗,还是面馆那种大碗?”澄清完再进入计算。
# nutrition.py FOOD_DB = { "米饭": {"heat": 116, "protein": 2.6, "fat": 0.3, "carb": 25.9}, # 每100g "馒头": {"heat": 223, "protein": 7.0, "fat": 1.1, "carb": 47.0}, "鸡胸肉": {"heat": 133, "protein": 19.4, "fat": 5.0, "carb": 2.5}, "西兰花": {"heat": 36, "protein": 4.1, "fat": 0.6, "carb": 4.3}, # ... 更多常见食物 } def estimate_meal_heat(foods: list) -> dict: """ 估算一餐的总热量和宏量营养素 Args: foods: [{"name": "米饭", "amount_gram": 150}, {"name": "鸡胸肉", "amount_gram": 100}] Returns: 汇总的营养数据 """ total_heat = 0 total_protein = 0 total_fat = 0 total_carb = 0 for item in foods: name = item["name"] gram = item["amount_gram"] if name not in FOOD_DB: continue # 或者返回错误让模型提示用户 per_100g = FOOD_DB[name] ratio = gram / 100.0 total_heat += per_100g["heat"] * ratio total_protein += per_100g["protein"] * ratio total_fat += per_100g["fat"] * ratio total_carb += per_100g["carb"] * ratio return { "total_heat_kcal": round(total_heat, 1), "protein_g": round(total_protein, 1), "fat_g": round(total_fat, 1), "carb_g": round(total_carb, 1) }这个函数本身的逻辑很简单,但集成到系统里有三个设计要点:
不要在函数内部内置“这个食物能不能吃”的判断。同一个食物对不同慢病患者的意义不同,糖尿病患者关注GI,痛风患者关注嘌呤,肾功能不全的人关注蛋白质。计算模块只负责“算出来是多少”,至于“这个人能不能吃”,那是基于用户画像的另一套规则系统的事。
换算要严格。用户如果说“喝了一碗粥”,你要通过对话澄清出“大米粥还是小米粥”“稠的还是稀的”,然后按对应食材去查表。知识库可以提供“一碗粥大约是多少克”的参考,但最终进入计算的一定是经过确认的数值。
3.4 运动建议中的能量消耗估算
运动建议这块,我的处理思路和饮食类似——计算模块负责估算“运动消耗了多少热量”,再结合用户的BMR和当天的摄入情况,给出热量差判断。
def estimate_exercise_burn(activity: str, duration_min: int, weight_kg: float) -> dict: """ 估算运动消耗热量(METs法) 消耗(千卡) = METs * 3.5 * 体重(kg) / 200 * 持续时间(分钟) """ METS = { "散步": 3.0, "快走": 5.0, "慢跑": 7.0, "游泳": 6.0, "骑自行车": 6.8, "瑜伽": 2.5, "打太极拳": 3.0, } if activity not in METS: raise ValueError(f"暂不支持的运动类型: {activity}") met = METS[activity] burn = met * 3.5 * weight_kg / 200 * duration_min return { "activity": activity, "duration_min": duration_min, "burn_kcal": round(burn, 1), "met": met }用METs公式估算运动消耗,虽然不能做到像专业运动手环那样精准,但在慢病管理的日常场景里已经足够。它是一个有生理学依据的近似值,而且只要输入确定,输出就完全确定——这满足了我们系统对“可复现性”的要求。
4. 系统集成:如何让大模型正确使用计算模块
计算函数写好了,只完成了30%的工作。真正决定系统成败的,是把这些函数接入大模型推理链路的“胶水层”设计。我见过太多项目死在最后这一步:函数写得很好,但模型各种调不对、调了不用、用了结果不给用户讲清楚。
4.1 Function Calling驱动的工具调用机制
目前主流的大模型平台都支持Function Calling。它的核心机制是:你在请求里声明有哪些工具(函数)可用,模型在生成回复之前,会先判断当前用户的输入是否需要调用某个工具,如果需要,就输出一个结构化的函数调用请求——注意,这时候模型并没有真的执行这个函数,它只是告诉你“我打算调用这个函数,参数是什么”。
这个“参数生成”环节是幻觉的高发区。模型有可能把参数名搞错、把数值格式弄错,甚至自己编一个函数名出来。所以工程上一定要做两层防护:
- 结构化输出约束:通过JSON Schema严格定义参数类型,比如身高必须是number、性别必须是枚举值,模型生成的参数要经过校验,不合格就重试或让模型追问用户。
- 工具白名单:系统只允许模型调用预先注册的、经过测试的函数。模型如果输出一个不在白名单里的函数名,直接判定为调用失败,引导模型重新组织语言。
下面以OpenAI风格的Function Calling为例,给一个接入慢病系统的参考实现。
# llm_tool_bridge.py """ 大模型与计算模块之间的工具桥接层 """ import json from typing import Callable, Dict from core_metrics import calculate_bmi, calculate_bmr, calculate_glucose_variability from nutrition import estimate_meal_heat from exercise import estimate_exercise_burn # 注册可被大模型调用全部工具 TOOL_REGISTRY: Dict[str, Callable] = { "calculate_bmi": calculate_bmi, "calculate_bmr": calculate_bmr, "calculate_glucose_variability": calculate_glucose_variability, "estimate_meal_heat": estimate_meal_heat, "estimate_exercise_burn": estimate_exercise_burn, } # 工具的JSON Schema描述,用于让模型理解何时触发 TOOL_SCHEMAS = [ { "type": "function", "function": { "name": "calculate_bmi", "description": "计算用户的身体质量指数(BMI),当用户提供身高和体重时调用", "parameters": { "type": "object", "properties": { "weight_kg": {"type": "number", "description": "体重,单位为公斤"}, "height_cm": {"type": "number", "description": "身高,单位为厘米"} }, "required": ["weight_kg", "height_cm"] } } }, { "type": "function", "function": { "name": "calculate_glucose_variability", "description": "计算血糖波动指标(平均值、标准差、变异系数),当用户提供多条血糖记录时调用", "parameters": { "type": "object", "properties": { "glucose_records": { "type": "array", "description": "血糖记录列表,每个元素包含value和time字段", "items": { "type": "object", "properties": { "value": {"type": "number", "description": "血糖值,单位mmol/L"}, "time": {"type": "string", "description": "测量时间,例如2025-01-01 07:30"} }, "required": ["value", "time"] } } }, "required": ["glucose_records"] } } }, # ... 其余工具schema ] def execute_tool_call(tool_name: str, arguments: dict): """ 安全的执行工具调用 1. 校验工具名是否在白名单中 2. 校验参数是否符合预期(可进一步用pydantic做约束) 3. 执行并返回结构化结果 """ if tool_name not in TOOL_REGISTRY: return { "status": "error", "message": f"未知的工具调用: {tool_name}" } func = TOOL_REGISTRY[tool_name] try: result = func(**arguments) return {"status": "success", "result": result} except TypeError as e: # 参数不匹配,返回错误信息,让模型自行处理 return {"status": "error", "message": f"参数错误: {str(e)}"} except ValueError as e: # 数值校验失败,比如身高不可能低于20cm return {"status": "error", "message": str(e)}在实际项目中,我把整个调用流程封装成了一个标准循环,描述在这段代码的注释里了:
第一步,把用户问题+可用的工具Schema一起发给大模型。第二步,模型返回一个工具调用请求,或者直接返回一个自然语言回答。第三步,如果是工具调用请求,系统执行对应的Python函数。第四步,把函数执行结果以“工具响应”的消息类型回传给模型。第五步,模型基于工具结果,生成最终面向用户的自然语言答复。
这个循环是标准的Agent模式。它的精妙之处在于:模型不再需要自己算数,它只需要负责把用户的自然语言转成结构化的函数参数,再把函数返回的结构化结果转回自然语言。两件都是语言模型最擅长的事,而计算本身交给了确定性的代码。
4.2 参数提取与校准:模型容易在此处翻车
我测试过很多模型的参数提取能力,结论是:大模型在从自由对话里提取结构化参数时,错误率远高于直接给一段说明文让它总结。这不难理解,因为自然语言里充满歧义、省略和模糊表达。
举一个真实测试中反复遇到的场景:
用户:“我身高一米七十五,体重这段时间一直在一百四十五斤左右,帮我算算BMI呗。”
这段话要正确解析,需要三个关键步骤:
- 单位统一:一米七十五 = 175厘米;一百四十五斤 = 72.5公斤。注意,“斤”和“公斤”是两倍关系,模型能干成1:1转换并不罕见。
- 模糊量词处理:“左右”这个词表示用户给的是一个近似值。这时候不应该直接拿“145斤”去算,而应该跟用户确认:“请问您最近的体重以72.5公斤计算可以吗?”或者取一个区间值,算出BMI的范围。
- 缺失信息补充:如果用户只报了体重没报身高,模型应该追问,而不是“猜一个常见身高”。这个行为要在System Prompt里明确约束,甚至要通过后置校验来兜底。
所以我强烈建议,在设计Tool Schema时,把每个参数的描述写得极其具体——不仅说明“这个参数是什么”,还要说明“什么情况下该填什么值”。同时,在System Prompt里写上一句硬性规则:“如果用户的表述中含有不确定的量词(比如‘左右’‘大概’‘约’),不要直接计算,先向用户确认准确数值。”
4.3 一个核心技巧:让模型复述计算过程
即使Function Calling机制很成熟,我还是建议在System Prompt里给模型加一个“解释计算过程”的要求。不是让模型自己算,而是让模型把计算模块返回的结果用自己的话复述一遍,让用户能看懂。
比如计算模块返回:
{"status": "success", "result": {"bmi": 23.7, "level": "正常", "advice": "您的BMI为23.7,属于正常范围。"}}模型应该怎么回复?我期望它说:
“我帮您算了一下,用身高175厘米和体重72.5公斤计算,您的BMI是23.7,这个数值在正常范围内(中国成人参考标准是18.5到24.0)。说明您目前体重控制得还不错,继续保持。”
而不是只丢一句话:“你的BMI是23.7。”
复述计算过程有两个作用:第一是让用户感受到系统是“真的算了”,而不是随口一说;第二是暴露潜在的逻辑错误——如果模型把23.7复述成“偏胖”,你一眼就能看出中间出了问题。
5. 实操案例:三场典型交互的完整实现
理论讲再多,不如看实际效果。这里我结合真实项目,模拟三个慢病AI陪伴系统的高频使用场景,每一步都附上完整的交互与中间数据,方便你照葫芦画瓢。
5.1 场景一:用户自查BMI和BMR
用户输入:“我今年45岁,男,身高172,体重80公斤,平时办公室坐一天不怎么动,帮我算算我的BMI和基础代谢。”
系统执行链路:
第1步:大模型解析意图 检测到用户提供了身高(172cm)、体重(80kg)、年龄(45)、性别(male), 且明确要求计算BMI和BMR,判定需要调用工具。 第2步:生成工具调用请求 [调用 calculate_bmi(weight_kg=80, height_cm=172)] [调用 calculate_bmr(weight_kg=80, height_cm=172, age=45, sex="male")] 第3步:计算模块执行结果 calculate_bmi -> {"bmi": 27.0, "level": "超重", "advice": "您的BMI为27.0,属于超重范围。"} calculate_bmr -> 1634.0 // Mifflin-St Jeor公式: 10*80 + 6.25*172 - 5*45 + 5 第4步:大模型综合生成话术 "先生,我帮您做了两个指标的计算。您的BMI是27.0,按中国成人标准属于超重范畴(正常范围18.5~24.0)。 考虑到您平时以久坐办公为主,您的基础代谢率(BMR)大约是1634千卡/天,这是您身体静息状态下 维持基本生命活动需要消耗的热量。如果需要进一步估算您每天的总消耗,我们可以再结合您的日常 活动量来推算。"这个回复的质量,明显优于让模型直接“临场发挥”的结果。关键数据全部来自代码计算,模型只做了自然语言的组装和润色。
5.2 场景二:用户上传一周血糖记录
用户输入:“我最近一周的血糖记录都在这里了,早上空腹分别是6.2、6.5、6.0、6.8、6.3、5.9、6.4,帮我看看波动大不大。”
系统执行链路:
第1步:大模型识别到这是一组时间序列血糖数据,触发calculate_glucose_variability工具。 第2步:调用工具 glucose_records = [ {"value": 6.2, "time": "空腹"}, {"value": 6.5, "time": "空腹"}, {"value": 6.0, "time": "空腹"}, {"value": 6.8, "time": "空腹"}, {"value": 6.3, "time": "空腹"}, {"value": 5.9, "time": "空腹"}, {"value": 6.4, "time": "空腹"} ] 第3步:计算模块返回结果 mean_glucose=6.3, std_dev=0.3, cv=4.8, max=6.8, min=5.9 血糖波动很小,处于良好控制状态。 第4步:大模型话术组装 "看您这周的空腹血糖,平均在6.3 mmol/L左右,最高6.8、最低5.9,变异系数不到5%, 说明您的血糖控制得相当平稳。不过从数值上看,空腹血糖整体还是略偏高一点,建议 继续关注晚餐摄入和早晨用药的配合情况。"注意对话中提到的时间信息被简化了,我们在系统里可以用“第1次空腹”或具体日期时间戳来标记。真实系统里不推荐让用户手动输入JSON数组,更好的方式是通过前端录入或导入设备数据,这里则是为了说明工具调用的参数传递逻辑。
5.3 场景三:饮食与运动综合建议
用户输入:“我身高165,体重65公斤,50岁女性,今天中午吃了一碗米饭、一份清蒸鱼和一份炒青菜,然后去快走了30分钟,帮我算算今天中午这顿饭的热量和运动消耗。”
系统执行链路:
第1步:模型拆分任务 识别出两个子任务: A. 估算饮食摄入热量和宏量营养素 B. 估算快走30分钟的运动消耗 第2步:调用两个工具 [调用 estimate_meal_heat(foods=[ {"name": "米饭", "amount_gram": 150}, {"name": "清蒸鱼", "amount_gram": 100}, {"name": "炒青菜", "amount_gram": 200} ])] [调用 estimate_exercise_burn(activity="快走", duration_min=30, weight_kg=65)] 第3步:工具返回结果 meal_heat = {"total_heat_kcal": 约250, "protein_g": 约28, "fat_g": 约6, "carb_g": 约45} exercise_burn = {"burn_kcal": 约170, "met": 5.0} 第4步:模型组装回答 "您午餐的热量估算大约是250千卡,蛋白质约28克,脂肪6克,碳水45克。这个搭配在三大 营养素比例上是比较均衡的,蛋白质略高一点,有助于延缓餐后血糖上升。您快走30分钟 的运动消耗大约在170千卡左右。这一餐的摄入量和运动消耗之间,动的部分补掉了一大部分 热量缺口,整体节奏是健康的。"这里有一个值得注意的点:模型在没有说明的情况下,自动挑了“炒青菜”作为食物条目,并估算了一个200克的重量。这个动作需要约束——应该在系统规则里要求模型对“用户没说重量”的食物进行反问,而不是自作主张地补一个数值。
6. 效果验证:测试结果与业务指标变化
方案做出来了,如何证明“可计算数学模块”确实能降低幻觉、提升系统质量?我建议从两个维度来验证。
6.1 幻觉压测:一组可执行的测试用例
围绕慢病AI最常见的问题,我整理了一组“基准测试集”,用来量化系统改进前后的效果。每组测试由“问题 + 期望答案类型 + 可接受误差范围”组成。
| 测试类别 | 示例问题 | 期望结果类型 | 可接受误差/判定标准 |
|---|---|---|---|
| 数值计算 | 身高175cm、体重70kg,BMI是多少 | 数值+分级 | 数值误差±0.1,分级正确 |
| 公式推算 | 45岁男性,BMR是多少 | 数值 | 数值误差±5千卡 |
| 数据统计 | 一周血糖的变异系数 | 数值+解读 | 数值精确,解读符合阈值 |
| 饮食记录 | 估算一碗米饭+鸡胸肉的热量 | 数值+营养素 | 数值在合理范围内,来源有依据 |
| 异常输入 | 血糖值58mmol/L | 拒绝计算并引导重新输入 | 不得给出任何健康解读 |
| 缺失信息 | 用户只说“帮我算BMI”,没说身高体重 | 追问信息 | 不得猜测数值 |
改进前,直接让大模型回答这些问题的整体通过率大约在六到七成,剩下三成多要么数值算错,要么在边界情况下给出误导性结论。引入计算模块后,数值类测试的通过率可以做到接近100%,剩下的失败案例全部集中在“模型调错工具”或“参数解析错误”上。这说明系统的确定性已经由计算层兜住,但模型的调度能力仍然值得关注。
6.2 业务层面的指标变化:从技术指标到用户价值
技术指标只是一部分,慢病AI系统最终要回答的问题是:它有没有让用户的健康行为发生正向改变?
我观察到一个非常明显的对比。在没有计算模块支持的版本里,用户对系统建议的信任度在对话进行到中后期会下降。原因很简单:用户发现AI说的东西不太稳定,有时候BMI给27.1,过两天重新问又变成26.9。这种“不靠谱感”是慢病陪伴的致命伤。而接入确定性计算模块后,同一个用户无论问多少遍,同样的输入永远得到同样的输出。这个简单的“一致性”本身,就极大提升了用户对系统的信任感。
另一个指标是用户主动上传数据的频率。当用户发现AI会认真对待自己上传的血糖记录、运动数据,并且能基于这些数据给出个性化分析时,用户上传数据的意愿会明显增强。数据量多了,系统可以做更精准的趋势分析,形成正向循环。
6.3 不推荐的做法:为什么不能用prompt硬约束替代计算模块
有些团队可能会想:既然大模型会算错,那我就在prompt里强调“你必须仔细计算”行不行?我可以直接告诉你,这个方案在工程上不可靠。
原因在于,大模型的推理机制是概率性的,它当前时刻生成“7.2”和下一篇生成“7.0”之间,没有本质的区别——都是概率采样。prompt可以让模型在大部分时候“表现得像在认真计算”,但它无法保证100%正确,尤其在你无法通过单元测试覆盖全部输入空间的边界情况下。而对于医疗健康场景,100%这个要求不是“高要求”,是“及格线”。所以,可计算模块不是prompt的替代品,而是所有prompt都失效时的最后防线。
7. 真实交付中的坑与排查建议
最后聊聊我在实际落地中遇到过的几个问题。这些坑很典型,你有很大概率也会踩。
7.1 血糖记录的数据清洗不能依赖大模型
很多用户上传血糖记录时,数据格式五花八门:“空腹6.5”“餐后2小时8.0”“睡前5.9”。有的带单位,有的不带;有的写“7.2mmol/L”,有的写“早上测的7.2”。如果让大模型直接解析并转换为结构化数据,它出错概率不低。
我的建议是,数据清洗分成两步:第一步,用规则和正则表达式做预处理,把明显能识别的格式先结构化;第二步,不能识别的、模糊的数据,走大模型做“二次理解”,但大模型输出后必须再由规则层校验一遍。没有规则层兜底的数据,绝不能直接进入计算模块。
7.2 工具调用的参数校验要写清楚返回语义
Function Calling链路里,如果参数校验失败,比如用户输入的身高是“1米7”,模型把它解析成了“1.7厘米”,计算模块抛出一个ValueError。这个错误信息回传给模型后,模型应该能理解“哦,参数单位不对,我要再问用户确认一下”。但如果错误信息写得不够清楚,模型可能直接把这个数字传给下一个工具,白白浪费一轮调用。
我建议在计算函数内部统一使用“错误信息 + 提示如何纠正”的返回结构。举个例子,单位校验失败时,返回的错误信息应该是:“身高数值异常:1.7cm,请确认是否把‘1米7’解析为170cm。”这样模型可以根据提示自动修正,而不是把错误原样抛给用户。
7.3 避免计算模块在业务逻辑里“过度膨胀”
最后一条经验是一个反模式警告。可计算模块的功能强大之后,很容易越做越重,最终把本该由业务规则、状态机、甚至人工运营负责的事情也塞进来。比如要不要提醒用户复诊、要不要调整用药方案,这些本质上属于需要医学知识和临床决策树的事情,不能简单用“公式”去解决。
我给自己定的边界很简单:计算模块只解决“给定确定的数,算出确定的结果”这类问题;涉及“要不要做、怎么做”的决策问题,交给独立的决策引擎或者引入人工审核。一旦计算模块开始尝试“做决策”,系统的可解释性就会急剧下降,出了问题你也很难向用户交代。
8. 写在最后:这是我能想到的最省力但最可靠的架构
做慢病AI陪伴系统这几年,我最深的体会是:大模型的幻觉问题不是靠“换个更大的模型”就能解决的,也不是靠“prompt写得再细一点”就能规避的。它在架构层面就注定了需要用确定性组件去兜底。
可计算数学模块就是这个确定性组件的核心。它不一定需要很复杂,几个公式、一张食物数据库表、一组统计函数,就能把系统里最容易出错、最影响信任感的数值类功能全部接管过来。大模型则退回到它最擅长的领域——理解用户、组织语言、表达温度。两者配合,系统的上限和下限都会提高一个档次。
如果这个思路对你有启发,建议你先从最简单的一个计算函数开始,比如就做一个BMI计算器接入你现有的对话系统,然后逐步扩展到血糖评估、饮食记录、运动消耗。模块边界划清楚之后,每一步的增量开发成本都很低,但系统可靠性的提升会非常明显。
最后再分享一个小技巧:给你的每个计算函数都写一段中文描述,说明“何时调用”“何时不应该调用”“参数怎么填”。这段描述会直接参与模型生成工具调用请求的决策,写得好,模型的调度准确率能高出一大截。这个投入产出比极高,强烈推荐你试试。