高考志愿AI这类项目,我见过太多团队一上来就奔着“AI一键生成志愿表”去做了。但我这次接手的高考志愿AI项目,反而是把“替用户做决定”这条路彻底堵死之后,数据才真正好看起来。这个系列前面聊了不少FDE落地实战的细节,这次借第12篇的机会,把决策辅助类AI的边界问题、推荐策略的工程实现、以及大模型输出控制这几个硬骨头一次讲透。如果你是做AI Agent、做智能化产品落地的工程师,或者正打算碰高考志愿、选科规划、职业测评这类“给建议”场景,这篇应该能帮你少走不少弯路。
先说背景。我们做的这个高考志愿AI,不是那种“输入分数就给你一张完美志愿表”的魔术工具,而是一个把数据整理、梯度分析、专业解读、风险提示打包在一起,最后把拍板权交还给考生和家长的辅助系统。整个项目中我负责的部分覆盖了从录取数据清洗、位次换算、冲稳保策略引擎,到基于大模型的解释生成和风险提示,算是一次完整的FDE全链路实践。这篇文章不打算铺开讲全部细节,重点放在“怎样给建议而不是替人做决定”这件事背后的工程取舍上。
1. 从“AI替你填”到“AI陪你填”:一次需求偏移的复盘
1.1 高考志愿场景的真实痛点:不是缺答案,是缺依据
高考志愿填报这个场景,表面上看是一个信息检索问题:你有分数,你有省排名,你需要知道哪些学校能上、哪些专业值得报。但真正把产品做深之后,你会发现它本质上是一个“高风险决策下的信息焦虑问题”。信息检索的标准答案是唯一的,就好比“今天北京气温多少度”,查一下就有明确结果。而志愿填报的问题几乎都是开放式的——“我这个分数,是选一个中流211的好专业,还是冲一冲985的冷门专业?”“留在省内读双一流,还是去省外城市读普通一本?”这种问题没有标准答案,每个选择背后都是一套价值排序,而价值排序这东西,数据是算不出来的。
我走访了不少考生和家长之后,一个很深的感受是:他们缺的不是“一个答案”,而是“做出判断的依据”。家长焦虑是因为看不懂动辄上千条录取数据的波动规律,考生焦虑是因为对多数专业只有望文生义的印象——觉得“金融”就是赚钱多,“计算机”就是写代码,“临床医学”就是当大夫。市面上的志愿卡产品大多只是把数据做成查询工具,本质上还是让用户自己在信息海里找答案,只是省了翻书的功夫。而很多打着AI旗号的产品又走到另一个极端,试图把决策本身包办了。这两个方向都有问题,前者没有真正降低决策门槛,后者越过了决策责任的边界。
我们第一版产品恰恰就踩了后者的坑。
1.2 用户访谈里挖出的三个反直觉结论
第一版产品上线时,团队的想法很直接:用户输入分数和省份,AI直接输出一张排好序的“冲稳保志愿表”。当时我们觉得这就是效率,就是AI的价值。结果上线两周,用户流失率非常高,留存数据让人看不下去。于是我们做了一轮针对性的用户访谈,陆续聊了近30组家庭,挖出了三个扎心的反直觉结论。
第一个结论:用户不直接看排名,而是先追问“为什么”。几乎每个人拿到推荐列表之后,第一反应不是“这个学校适合我”,而是“这东西凭什么排在第一?”追问率超过70%。这说明在用户心里,AI推荐一开始是没有信任背书的,它必须给出能被验证的理由链条,用户才敢往下看。
第二个结论:用户信任的标准不是“准确率”,而是“逻辑可见”。访谈里很多家长说,他们并不是真的能判断哪个学校更好,但他们需要看到推荐理由是连贯的、有数据支撑的。一条推荐理由如果包含“该校该专业近三年位次稳定在8000到10000名之间,你的位次是8600名,属于比较稳妥的选择,但需要注意今年该专业在省内扩招了15%”这样的完整逻辑链,用户的信任度会明显上升。而如果只是说“推荐理由:该学校综合实力强”,哪怕数据再准,用户也不买账。
第三个结论可能最反直觉:用户要的是“决策材料”,不是“决策本身”。我把这个结论盯了很久才接受——考生和家长在志愿填报这件事上,内心深处是想“自己做主”的,他们只是苦于没有足够好的材料来支持自己做这个主。填报志愿是孩子人生里第一次重大的自主选择,家长参与其中是希望“把风险看清楚之后再把关”,而不是把选择权交给一个黑盒。哪怕这个黑盒给出的方案是全局最优的,只要用户不理解它,就不会接受它,更不敢为它的结果负责。
这三个结论直接推翻了我们对产品的定义。系统需求从“生成最优志愿表”变成了“生成决策参考材料”,AI的定位从“专家”变成了“参谋”。整个项目在第二周全部重构。
2. FDE视角下的系统拆解:决策辅助AI的功能边界
2.1 三个分层:数据层、策略层、表达层
重新调整方向之后,我做的第一件事是画清系统的功能边界。决策辅助类AI和普通推荐系统最大的区别在于,它不能只输出结论,必须输出“从数据到结论的完整通道”。所以我把整个系统拆成三个分层:数据层、策略层、表达层。这三个层的职责边界一开始就必须定死,否则后续一定会出现策略层拿到了不该有的判断权,或者表达层丢失了关键数据这种混乱。
数据层负责把原料准备好。这个阶段要处理的包括各省历年录取分数、院校最低投档位次、专业录取均分、招生计划数、批次控制线、院校属性标签(双一流、985、211、普通本科),还有就业质量报告里的就业率、升学率、平均薪资等字段。数据层的产出不是“一堆能看的数据”,而是“经过对齐和清洗、可以直接进入计算引擎的结构化宽表”。这一步在工程上看着不起眼,但其实是最容易翻车的地方——不同省份公布的数据格式差异非常大,同一个院校在不同年份的名字可能是“XX大学”和“XX大学(XX校区)”,同一所院校的招生代码还年年变。不把这些坑填平,后面的策略再漂亮也是空中楼阁。
策略层是推荐引擎的本体。它接收数据层的宽表,结合用户输入的分数、省排名、选科组合、地域偏好、专业意向,输出一个结构化的候选列表。注意策略层的输出格式不是自然语言,而是一份JSON结构,每个候选院校带上一组字段:录取概率区间、梯度标签(冲/稳/保)、推荐分数、匹配专业清单、风险等级。这个设计是我特意坚持的——策略层只负责“计算”,不负责“解释”,解释的事全部交给表达层。
表达层才是大模型发挥作用的地方。策略层输出的结构化结果被组装成上下文,由大模型生成一段自然语言推荐说明。这一段说明必须覆盖四个要点:推荐理由是什么、数据依据是什么、不确定性在哪里、在什么前提下这个推荐可能不成立。为了让表达层稳定地做到这一点,我给每次调用都套了一个严格的输出模板,所有数据字段都被编排成模板里的槽位,模型只负责把槽位之间用流畅的语言串起来,不负责发明新数据。
借用一张表概括三个分层的分工:
| 分层 | 输入 | 核心产出 | 是否允许自由生成 |
|---|---|---|---|
| 数据层 | 各省历年录取数据、招生计划、就业数据 | 对齐后的结构化宽表 | 否,字段级校验 |
| 策略层 | 宽表 + 用户画像 | 候选院校JSON,含概率区间、梯度标签、风险等级 | 否,纯规则计算 |
| 表达层 | 策略层JSON + 模板 | 带推荐理由、风险提示的自然语言说明 | 部分允许,数据槽位固定 |
这个分层的核心思想就一句话:把判断留给策略层,把表达留给大模型,把数据牢牢锁在数据库里。大模型永远接触不到原始数据库,它只能看到策略层给它的那几行JSON,这就从根本上杜绝了大模型“编数据”的空间。
2.2 为什么“推荐结果必须能被解释”是第一需求
高考志愿这个场景里,推荐结果一旦出错,用户要承担的后果不是“点了个不合适的餐厅”那么简单,而是可能直接滑档、退档、被调剂到完全没想过的专业,影响未来四年甚至更久。这种后果决定了用户对AI系统的要求一定是“不仅要告诉我怎么选,还要告诉我为什么这么选,以及这个选择的风险在哪里”。
从工程上落地“可解释”这件事,我有两个具体的建议。第一,策略层做每一步计算的时候,必须把中间结果单独落一份日志。比如等位分换算的原始位次、修正系数、换算后的位次;梯度划分的概率区间、参考的基础数据年份;专业匹配评分的每个维度的子分数。这些中间结果不直接展示给用户,但它们是大模型生成解释内容时的“事实底稿”。没有这份底稿,表达层就只能对着最终结论硬编理由,编出来的内容跟实际计算过程脱节。
第二,给表达层的模板里设计一个“理由字段”的强制要求。每条候选院校的推荐都必须在策略层输出里附带一条“机器可读的理由摘要”,例如:
{ "school_id": "A101", "gradient": "稳", "prob_range": [0.55, 0.68], "match_score": 0.72, "reason_tags": ["位次匹配", "专业方向一致", "选科要求满足"], "risk_flags": ["该专业今年缩招20%,概率区间下限需警惕"] }表达层在生成自然语言的时候,只能围绕reason_tags和risk_flags展开,不能另起炉灶。这实际上是把“解释权”从模型手里收回了一部分,模型的工作变成了“组织语言”,而不是“构造理由”。理由本身是由数据计算出来的,这个边界守住了,“可解释”才不是一个口号。
3. 志愿推荐策略的工程实现:从位次法到冲稳保梯度
3.1 数据清洗与等位分换算的细节
高考志愿推荐最底层的算法,业内通常用两种方法:位次法和线差法。位次法的逻辑是,用考生今年的省排名,去对比往年录取学生的省排名区间,判断录取概率;线差法是用考生分数和批次线的差值,去对比往年录取线和批次线的差值。位次法在填报人数多、分数区分度高的省用得多;线差法在部分批次线波动大的省份会更稳定一些。
我们系统的主策略也是位次法,但实际工程里有一个不得不处理的细节:位次不是可以直接跨年比较的。每年高考的考生总数在变、招生计划在变、高考试卷难度也在变,同样的位次在不同年份对应的“含金量”并不一样。所以我第一步是把各校历年录取位次做一次跨年对齐,具体做法是引入“等位分”概念——把不同年份的位次按照当年的一分一段表映射到一个统一的尺度上,再进行对比。
举一个简化的计算例子。假设某校2023年在该省的最低录取位次是18000名,2024年考生总人数增加了8%,招生计划保持不变,那么2024年的18000名对应的竞争强度实际上比2023年的18000名更高。换算时我会引入一个“人数膨胀系数”,用去年考生总人数除以今年考生总人数得到系数,再乘到去年的位次上,做加权调整。再结合近三年数据的指数平滑,给最近一年的数据更高的权重,公式类似:
import pandas as pd def align_rank(raw_series, weight=[0.5, 0.3, 0.2], expansion_factor=[1.0, 0.96, 1.02]): """ 对近三年录取位次做等位分对齐和指数平滑。 raw_series: 按年份升序的原始录取位次列表 weight: 最近三年权重,默认最近一年权重最高 expansion_factor: 各年份考生人数膨胀系数 """ adjusted = [r * e for r, e in zip(raw_series, expansion_factor)] smoothed = sum(a * w for a, w in zip(adjusted, weight)) return smoothed这个例子做了大量简化,实际项目里系数不是拍脑袋定的,而是拿往年真实录取结果做回测调出来的。调参的时候我们以“回测命中率”作为指标——拿2023年的数据生成推荐,看2022年的实际录取结果是否落在推荐范围里,多次迭代找到最稳的参数组合。
踩过的坑也要提一下:某些“断档”年份的数据是必须剔除的。比如某校某年因为遇冷,录取位次突然从往年的一万名掉到两万名开外。这种异常值放进平滑模型里会把预测区间扯得很离谱。我处理的方式是检测标准差超过阈值的数据点,单独剔除,并在推荐理由里注明“该校2022年录取位次异动较大,参考价值有限”。
3.2 冲稳保三档的划分逻辑与参数调优
“冲稳保”是志愿填报里最常见的策略框架,人人会喊,但不同产品对三档的划分方式差别很大。粗暴的做法是直接用位次差:你的位次比学校往年录取位次高2000名就是稳,低2000名就是冲。问题是位次差不是一个概率概念,它没法表达“冲”到底有多少机会,“稳”到底有多大概率被录取。
我做了一套基于历史位次分布的录取概率模型。核心思路是把每所学校近五年的录取位次看作一个样本集合,用分位数拟合出考生位次被录取的经验概率。简化版本如下:如果考生位次比某校五年录取位次的第90百分位还靠前,那么可以认为录取概率在85%以上,标记为“保”;如果位次落在第50到第90百分位之间,录取概率在40%到70%之间,标记为“稳”;如果位次刚够到第10到第50百分位之间,录取概率在10%到40%之间,标记为“冲”。
| 梯度 | 考生位次相对历史录取位次的位置 | 经验录取概率 | 志愿表建议占比 |
|---|---|---|---|
| 冲 | 前10%~50%区间 | 10%~40% | 30%左右 |
| 稳 | 前50%~90%区间 | 40%~70% | 40%左右 |
| 保 | 前90%以上 | 70%~95% | 30%左右 |
这套划分方式的好处是,每个梯度都对应一个可以解释给用户听的概率区间,不再是玄学。用户在最终生成的志愿表里,能清清楚楚地看到每个院校被标注的录取概率,而不是看到“冲”“稳”“保”三个字然后自己猜含义。
参数调优上,我不能只说概率,还得提一提“边界重叠”的处理。因为概率本身是连续的,概率40%的学校和概率70%的学校中间会有一条模糊地带,两个梯度之间出现交叉是常态。我处理的方式是不做硬切分,而是允许标注“稳偏冲”“稳偏保”这种过渡标签,让策略层保留细微次序,避免把两个概率相近、本质类似的选项硬分到两个档位,给用户制造不必要的困惑。
3.3 专业推荐的复合评分模型
院校推荐只是第一层,用户真正纠结的往往是专业方向的选择。专业推荐这块我们用的是复合评分模型,核心是解决一个问题:在考生的偏好和专业的客观数据之间找一个均衡点。
评分模型的输入分成两大块。一块是考生偏好侧,包括城市偏好(是否愿意去偏远地区)、学科兴趣标签(通过选科组合和兴趣测评得到)、对热门专业的接受度;另一块是专业数据侧,包括专业历年录取位次、就业率、升学率、薪资中位数、专业对口度。两块数据分别归一化到0到1之间,再加权求和。
权重怎么定?我先用层次分析法(AHP)做了一版基础权重,再通过真实用户的反馈做贝叶斯调整。比如有用户明确表示“我不想离开本省”,那么地域维度权重会被调高;有用户选科选了化学和生物,那么医药、生物工程类专业方向的相关度权重就会提高。每一版推荐结果都让用户点“喜欢/不喜欢/没感觉”三个按钮,用隐式反馈持续更新权重。实践证明,复合评分模型不能只用客观数据,必须允许用户画像动态调整权重,否则推荐的专业再“合理”,用户也大概率觉得“违背我自己的想法”。
这里还有一个我后来才补上的细节:专业匹配度不等于录取概率。用户可能对某个专业非常感兴趣,但该专业录取位次远超考生的水平。这种情况下我不会强行推荐高录取概率的专业,而是在专业清单里同时保留“高匹配低概率”和“低匹配高概率”两个方向,然后在表达层如实告诉用户:“你更感兴趣的A专业属于冲刺目标,而B专业与你画像匹配度略低,但这是一个更稳妥的选择。”把选择空间打开,让用户自己权衡,这是决策辅助必须守住的克制。
4. 输出控制:如何让AI的建议像同事不像上帝
4.1 给大模型的指令模板与约束
策略层把结构化结果算完之后,接下来就是表达层的活儿。最开始我犯过一个典型错误:直接用自由prompt让大模型生成推荐语。结果模型自由发挥的空间一大,虽然单个看每句话都很通顺,但连起来就出现了前后矛盾、过度承诺、甚至编造数据的问题。尤其是编造数据,这在高考志愿产品里是大忌,一次都不能出现。
后来我改成了模板化生成方案。大模型的角色被设定成一个“招生数据分析师助理”,而不是“志愿填报专家”。它的任务不是给结论,而是把给定数据组织成“决策参考材料”。我在prompt里做了非常明确的约束:
你将扮演一个高考志愿决策辅助助手。你的任务不是替用户做决定,而是基于以下结构化数据, 生成一份帮助用户理解选项优劣势的参考说明。请严格遵守以下规则: 1. 只能使用输入中提供的数据字段,严禁编造任何数据。 2. 每条推荐必须按顺序包含:推荐理由、数据依据、不确定性、风险提示。 3. 表达语气保持中立克制,不得使用“一定”“肯定”“绝对能上”等绝对化表述。 4. 出现概率时,必须以区间或模糊量词表达,不能给出单一精确值。 5. 最后必须附上一句提示,引导用户结合自身情况做最终判断。实际调用时的结构长这样:用户画像摘要 + 候选院校JSON + 输出模板描述。模型要做的事情是把JSON里的字段翻译成人话,再用模板结构串起来。实测下来,输出稳定性和合规性提升非常明显,重点不是让模型“更聪明”,而是让它“少自由发挥”。
我建议所有做类似产品的团队,都把大模型的输出从“自由作文”改成“结构化填充”。你保留一点灵活性让它组织措辞,但整个框架一定要由数据字段和模板严格约束。这一步治标的力度远大于换更强的模型。
4.2 不确定性表达:概率、区间、前提条件
给建议和替人做决定,在表达上有一条极其重要的分界线:是否如实呈现不确定性。替人做决定的AI说话是斩钉截铁的——“你适合报这个专业”“你这个分数可以冲这个学校”;给建议的AI说话是带着完整前提和概率区间的——“根据近三年数据,你被该校录取的概率大约在50%到65%之间,但需要注意该专业去年录取位次上行明显,若今年报考热度继续上升,概率区间下限可能进一步下移。”
我把不确定性表达拆成三个维度来做。第一个维度是概率表达,所有预测结论必须给出区间,拒绝单点值。10%到30%可以写成“有希望但机会有限”,40%到70%写成“具备一定优势”,75%以上才允许使用“稳妥”这类词。第二个维度是置信度表达,系统会给推荐结果附一个整体置信度标签,如果某所学校近五年录取数据波动很大,置信度就会被策略层标为“较低”,表达层必须如实告诉用户“该校历史数据波动较大,以下判断仅供参考”。第三个维度是前提条件保留,所有推荐说明里都必须出现类似于“以上分析基于该省2024年招生计划相对往年不变的前提”这样一句话。
可能有人会觉得,加这么多前提条件是不是太保守了,显得AI不专业?我实测下来的结论恰恰相反。考生和家长在志愿填报场景里的诉求,本质上是“想在不确定里降低风险”,他们真正反感的是那种不容置疑的语气。一个愿意讲清楚风险和不确定性的助手,比一个满口打包票的“专家”可信得多。你不妨回想一下现实中咨询师和填志愿机构的区别,你会发现用户愿意付费的那一类,往往都是把风险讲透的那一类。
4.3 风险提示与人工复核机制
除了表达上的不确定性,还必须有专门的风险提示模块。高考志愿填报里最危险的三个风险点:滑档、退档、专业调剂。滑档是用户填的所有院校都够不着,直接掉到下一批次;退档是院校投档分数够了,但因为专业不服从调剂或身体条件不符合要求被踢回来;专业调剂是被调剂到完全不相干的专业。
策略层在生成志愿表的时候,必须对这三类风险做显式标注。我给每条推荐都打了一个风险等级字段,分三级:绿色代表“录取概率高且专业方向匹配”,黄色代表“存在明显不确定性,如专业录取位次波动大或存在单科分数要求”,红色代表“存在退档或滑档显著风险,如只填了热门专业且不服从调剂”。红色风险项在前端必须弹窗确认,用户得主动点击“我已了解该风险”才能继续。
人工复核机制也很重要。系统会生成一份“志愿表诊断报告”,逐条列出每条志愿的风险点和对应的替换建议。这份报告支持导出,方便用户家长或者学校老师帮忙把关。有些用户还会拿着报告去当地教育考试院咨询,这是好事——AI给出的建议只是材料,而不是结论,用户越敢拿这份材料去找真人复核,说明我们的定位守得越成功。
5. 实测中的意外与迭代
5.1 用户最常问的“为什么给我推荐这个学校”
产品跑了第一轮小流量之后,我最关心的是行为日志。结果很快发现一个非常有意思的现象:用户点击“为什么推荐这所学校”的触发率,高得超过预期。单条推荐卡片上有个“解读”按钮,点开之后会展开一段生成好的推荐理由。日志显示,阅读完整解释的用户,后续把这条院校加入志愿草稿表的比例,比没点开解读的用户高出接近两倍。
这个数据直接验证了一个判断:市面上很多志愿卡产品做得不够好,不是推荐算法不行,而是推荐结果跟用户之间缺了一座“理解”的桥。用户看到一个学校出现在名单里,第一反应不是“太好了我有学上了”,而是“你凭什么觉得它适合我?”我们的解读按钮就是这座桥,它的存在让整个推荐过程从“黑盒给结果”变成了“白盒给推理”。
这也让我吸取了一个教训:推荐理由不能是马后炮式的一行小字,而应该是一个被精心设计的产品模块。在最早的版本里,推荐理由被塞在列表底部当注脚,几乎没人会往下翻到那里。后来我把“解读”入口直接放在了推荐卡片的封面位置,让它在视觉上跟院校名称同级,这才把用户注意力真正引导到解释内容上。永远不要低估解释在决策辅助产品里的产品地位。
5.2 从追问率看解释质量
既然“为什么推荐这所学校”的点击行为是关键路径,我就把这个按钮的点击率定义成“追问率”,当作解释质量的核心代理指标。追问率本身并不等于解释得好不好,它同时受到好奇心和质疑欲的影响,但它是一个极其灵敏的用户行为信号。追问率高不一定是坏事,它只说明用户对推荐的每一步都保持着审慎。
为了检验解释文本本身的质量,我做了一轮A/B实验。A组看到的推荐理由是用模板化生成的老版本,特点是严谨但比较干巴巴,数据说完就结束了;B组看到的是优化后的新版本,在末尾加了一句“如果你更看重城市发展而非院校名气,也可以考虑同档位另一所位于一线城市的院校”,给出可选择的替代方向。结果B组的追问率比A组低了约22%,但把推荐保存进草稿的比例反而上升了15%。
这说明,解释质量的提升方向不是“说服用户接受推荐”,而是“帮用户建立自己的判断框架”。当用户看完解释,感觉不是“AI让我选这个”,而是“我现在清楚各个选项的利弊了,我大概知道该怎么选了”,追问自然就少了,信任和行动意愿反而上升。从此之后我们所有文案优化的目标都锁定在这一个方向:减少替用户做决定的压迫感,增加帮用户理清思路的辅助感。
5.3 日志驱动的prompt调优闭环
最后聊一聊迭代闭环。推荐理由这种自然语言内容,需要持续打磨,不能靠开发人员隔三差五凭感觉改一版。我搭了一套比较轻量的迭代流程,核心是一个评测集加一份自动报告。
评测集是拿过去两年的真实志愿填报案例攒的,每一份包含用户的分数、位次、偏好输入,以及对应的策略层JSON输出。每次改prompt,我都会让新版本批量跑一遍评测集,然后抽样让团队按三个维度打分:数据准确性、逻辑完整性、语气适度性。数据准确性是最核心的,只要出现编造数据的样例,这一版直接打回重写。
自动报告跑的是线上日志。每天凌晨拉取昨天的追问率、解释展开率、风险提示确认率、推荐保存率,按推荐类型做交叉统计。哪个场景的解释点击率异常高,说明那个场景用户信任度低,需要定向调优。比如有一段时间“医学类院校”的解释追问率明显偏高,排查后发现是因为策略层对临床医学专业的录取概率估算偏高,用户看完觉得跟自己的预期对不上,才会反复追问。问题并不在解释层,在策略层的算法参数上。这提醒了我:日志驱动的不只是文案优化,有时候你顺着日志追下去,会发现真正的问题出在更上游的某个计算环节上。
整套闭环跑下来最直接的收益是,我们改prompt不再是拍脑袋,每一次改动都有数据反馈。做AI产品落地,最怕的就是上线之后靠玄学调参。有这份日志在,至少能知道每一步调整是在变好还是在变坏,这对FDE来说才是可持续的工程姿态。
做这个项目给我留下的最深感受是,决策辅助类AI产品,真正的难度不在算法,而在克制。模型能力再强,也要把最后的决定权还给用户;数据再全,也要承认预测本身是有边界的。作为研发,我们唯一能做的就是把这套系统打磨成一摞靠谱的“决策材料”,至于用户最后怎么选,那是他们的人生,不该由一行代码替他拍板。