文科分析引擎:让抽象知识"情境化"落地
一句话摘要:
HumanitiesAnalyzer以"情境化记忆"为方法论,用 0.3 的采样温度和 8 个完全不同字段的输出 schema,把政史地知识点加工成"典故 + 新闻 + 民俗 + 口诀"的记忆素材,并与理科引擎共享同一条落库流水线。
一、引言
文科学习的痛点,不在"理解不了",而在"记不住"。
"辛亥革命的历史意义"背了三遍还是混,"季风气候成因"理解了但考场上想不起术语。神经科学和教学法都指向同一个结论:孤立的信息记不住,被"挂"在故事、场景、联想上的信息忘不掉 。所以文科私教的第一性原理不是"讲清楚",而是"制造记忆钩子"------每个知识点都要钩住一个典故、一则新闻、一段民俗、一句口诀。
这就是「智学文枢」文科分析引擎的定位:它不输出"分步解析"(那对文科意义不大),而是把知识点翻译成一组"记忆素材"。services/ai/humanities_analyzer.py 与理科引擎(第 32 篇)共享相同的流水线骨架,却在三个维度上刻意分化:温度 (0.1 → 0.3)、输入 (不需要学生答案)、输出 schema(8 个字段完全不同)。
二、核心原理
2.1 文科的"记忆钩子"模型
文科分析要产出的不是"解题路径",而是五类钩子:
#mermaid-svg-tA4l2qqBQFpYMUOC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tA4l2qqBQFpYMUOC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tA4l2qqBQFpYMUOC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tA4l2qqBQFpYMUOC .error-icon{fill:#552222;}#mermaid-svg-tA4l2qqBQFpYMUOC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tA4l2qqBQFpYMUOC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tA4l2qqBQFpYMUOC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tA4l2qqBQFpYMUOC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tA4l2qqBQFpYMUOC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tA4l2qqBQFpYMUOC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tA4l2qqBQFpYMUOC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tA4l2qqBQFpYMUOC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tA4l2qqBQFpYMUOC .marker.cross{stroke:#333333;}#mermaid-svg-tA4l2qqBQFpYMUOC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tA4l2qqBQFpYMUOC p{margin:0;}#mermaid-svg-tA4l2qqBQFpYMUOC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tA4l2qqBQFpYMUOC .cluster-label text{fill:#333;}#mermaid-svg-tA4l2qqBQFpYMUOC .cluster-label span{color:#333;}#mermaid-svg-tA4l2qqBQFpYMUOC .cluster-label span p{background-color:transparent;}#mermaid-svg-tA4l2qqBQFpYMUOC .label text,#mermaid-svg-tA4l2qqBQFpYMUOC span{fill:#333;color:#333;}#mermaid-svg-tA4l2qqBQFpYMUOC .node rect,#mermaid-svg-tA4l2qqBQFpYMUOC .node circle,#mermaid-svg-tA4l2qqBQFpYMUOC .node ellipse,#mermaid-svg-tA4l2qqBQFpYMUOC .node polygon,#mermaid-svg-tA4l2qqBQFpYMUOC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tA4l2qqBQFpYMUOC .rough-node .label text,#mermaid-svg-tA4l2qqBQFpYMUOC .node .label text,#mermaid-svg-tA4l2qqBQFpYMUOC .image-shape .label,#mermaid-svg-tA4l2qqBQFpYMUOC .icon-shape .label{text-anchor:middle;}#mermaid-svg-tA4l2qqBQFpYMUOC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tA4l2qqBQFpYMUOC .rough-node .label,#mermaid-svg-tA4l2qqBQFpYMUOC .node .label,#mermaid-svg-tA4l2qqBQFpYMUOC .image-shape .label,#mermaid-svg-tA4l2qqBQFpYMUOC .icon-shape .label{text-align:center;}#mermaid-svg-tA4l2qqBQFpYMUOC .node.clickable{cursor:pointer;}#mermaid-svg-tA4l2qqBQFpYMUOC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tA4l2qqBQFpYMUOC .arrowheadPath{fill:#333333;}#mermaid-svg-tA4l2qqBQFpYMUOC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tA4l2qqBQFpYMUOC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tA4l2qqBQFpYMUOC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tA4l2qqBQFpYMUOC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tA4l2qqBQFpYMUOC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tA4l2qqBQFpYMUOC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tA4l2qqBQFpYMUOC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tA4l2qqBQFpYMUOC .cluster text{fill:#333;}#mermaid-svg-tA4l2qqBQFpYMUOC .cluster span{color:#333;}#mermaid-svg-tA4l2qqBQFpYMUOC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tA4l2qqBQFpYMUOC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tA4l2qqBQFpYMUOC rect.text{fill:none;stroke-width:0;}#mermaid-svg-tA4l2qqBQFpYMUOC .icon-shape,#mermaid-svg-tA4l2qqBQFpYMUOC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tA4l2qqBQFpYMUOC .icon-shape p,#mermaid-svg-tA4l2qqBQFpYMUOC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tA4l2qqBQFpYMUOC .icon-shape .label rect,#mermaid-svg-tA4l2qqBQFpYMUOC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tA4l2qqBQFpYMUOC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tA4l2qqBQFpYMUOC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tA4l2qqBQFpYMUOC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 知识点/习题
core_concept 一句话概括
historical_anecdote 历史典故
注明出处
current_news 近期新闻/政策
半年内+来源
folk_custom 民俗/风物
地理学科必填
memory_mnemonic 记忆口诀
exam_frequency 考试频次
common_mistake 易错点
落库 AIAnalysis
analysis_type=humanities
每条钩子都由第 29 篇的 HUMANITIES_USER 模板约束:典故要"注明出处"、新闻要"半年内"、民俗要"当地"------这些约束不是装饰,是为了防止模型编造(出处可查、来源可核)。
2.2 温度 0.3:创造性是刚需
理科压温度是为了"结论稳定",文科抬温度是为了"联想发散":同一个知识点,需要模型在不同次分析里关联不同的典故、给出不同的口诀,才有新鲜感。0.3 是实测平衡点------低于 0.2 时典故容易重复且老套,高于 0.4 时开始出现张冠李戴的史实错误。
2.3 输入差异:没有标准答案的学科
文科分析不接受 student_answer / correct_answer(模板里压根没有这两个占位符),因为政史地题往往没有唯一答案。输入只需要"知识点/习题 + 年级 + 学科"三要素,其余(当前水平/教材版本/考试目标)走第 30 篇的配置合并注入。
三、代码实现
3.1 组装与调用
python
# 源码位置:services/ai/humanities_analyzer.py L16-L37
def analyze(self, question, grade: str = "", config: dict = None) -> AIAnalysis:
"""分析文科习题/知识点
Args:
question: Question ORM对象
grade: 学生年级
config: Prompt配置字典,含学科开关、语气、偏好等
Returns:
AIAnalysis ORM对象
"""
# 构建Prompt(带配置注入)
messages = PromptTemplates.build_humanities_prompt_with_config(
subject=question.subject,
question_text=question.question_text,
grade=grade,
config=config,
)
# 调用AI前,记录完整提示词供核对组成
log_prompts("AI分析(文科)", messages, temperature=0.3)
raw_response = self.client.chat_json(messages, temperature=0.3)
与理科唯一的入参差异是少了 student_answer / correct_answer 两个参数------这印证了模板即数据契约:模板里有什么占位符,引擎就传什么参数,两者必须严丝合缝。
3.2 解析与兜底
python
# 源码位置:services/ai/humanities_analyzer.py L39-L51
# 解析JSON
parsed = extract_json(raw_response)
if not parsed:
parsed = {
"core_concept": "",
"historical_anecdote": "",
"current_news": "",
"folk_custom": "",
"memory_mnemonic": "",
"cross_subject_link": "",
"exam_frequency": "",
"common_mistake": "",
}
注意兜底字典里没有 knowledge_gap="AI返回格式异常,请重试" 这一条------这是与理科引擎的有意差异:文科的"核心概念"字段(core_concept)在 UI 上是大标题位,若填入"格式异常"会显得很突兀;文科格式异常时整体留白,UI 侧用占位样式提示,观感更自然。同一个兜底策略,也要按业务微调。
3.3 字段映射与落库
python
# 源码位置:services/ai/humanities_analyzer.py L53-L68
# 构建AIAnalysis对象
analysis = AIAnalysis(
question_id=question.id,
analysis_type="humanities",
raw_response=raw_response,
step_by_step=[],
core_concept=parsed.get("core_concept", ""),
contextual_memory=parsed.get("historical_anecdote", ""),
memory_mnemonic=parsed.get("memory_mnemonic", ""),
common_mistake=parsed.get("common_mistake", ""),
cross_subject_link=parsed.get("cross_subject_link", ""),
knowledge_gap=parsed.get("exam_frequency", ""),
)
# 存入数据库
return self.repo.create_or_update(analysis)
映射表里有两个"语义借用":
| JSON 字段 | AIAnalysis 列 | 语义说明 |
|---|---|---|
| historical_anecdote | contextual_memory | 典故归入"情境记忆"列 |
| exam_frequency | knowledge_gap | 考试频次借道"知识断层"列 |
| current_news / folk_custom | (未映射) | 存 raw_response,UI 从原文提取展示 |
exam_frequency → knowledge_gap 是最"抽象"的一处复用:数据库没有"频次"列,而 knowledge_gap 是通用文本列,把"高频/中频/低频"存进去,UI 层按 analysis_type 决定如何解读。step_by_step=[] 硬编码空列表,避免 ORM 列出现 None 破坏 JSON 列约束。
四、设计权衡 / 踩坑记录
1. 为什么文科要 8 个字段、理科也要 8 个,却不共用 schema? 第一版试图用一套"通用分析字段"兼容两科,结果是理科嫌字段太虚、文科嫌字段太硬。拆成两套 schema 后,各自字段贴合各自方法论(理科:步骤/公式/断层;文科:典故/新闻/口诀),代价是数据库列出现语义复用(knowledge_gap 存频次)------宁可列名抽象,不可字段错位。
2. 典故出处怎么防编造? 模板要求"注明出处",但模型仍可能编一个看似真实的出处。工程上无法完全杜绝,只能三管齐下:模板约束(注明出处)、温度压制(0.3 防过度发散)、UI 标注(典故/新闻区块标"AI生成,请核对")。提示词只能降概率,不能消风险,产品层必须给用户"核对"的预期。
3. 为什么 step_by_step=[] 而不是 None? SQLite 的 JSON 列存 None 会变成 SQL NULL,而 UI 侧代码普遍按列表处理 step_by_step(如 len(steps)、for s in steps),NULL 会直接炸。硬编码 [] 保证类型恒为 list------兜底值要按"下游消费方式"选择,而不是按"字段本义"选择。
4. 温度 0.3 是玄学还是工程? 0.3 不是拍脑袋:实测了 0.1/0.2/0.3/0.5 四档,0.1 时典故重复率高且过于严肃(像百科词条)、0.5 时开始出现史实张冠李戴、0.3 在"发散度"与"准确性"之间交点上。这个值写在 chat_json(messages, temperature=0.3) 里,同时被 log_prompts 记录,方便日后按样本复盘调参。
五、小结
| 维度 | 理科引擎 | 文科引擎 |
|---|---|---|
| 温度 | 0.1(求稳定) | 0.3(求发散) |
| 输入 | 题目+学生答案+正确答案 | 知识点/习题 |
| 核心产出 | 分步解析、断层诊断 | 典故、新闻、民俗、口诀 |
| 兜底策略 | 断层列提示"格式异常请重试" | 全字段留白 |
| 数据库列 | core_concept 存公式 | core_concept 存概念、knowledge_gap 存频次 |
HumanitiesAnalyzer 与 ScienceAnalyzer 一母同胞、各走各路:骨架完全一致(组装→留痕→调用→解析→落库),业务语义完全分化。这种"共享架构、分化语义"的模式,正是 AI 服务层后续所有引擎(变式/教学/跟进)的标配------下一篇的变式引擎将第一次引入"数组输出 + 显式重试",让我们看看骨架如何再进化。