摘要: 在开发一个考前测验AI Tutor系统的过程中,我踩了6个典型的LLM+Agent工程坑:无效选项被误判、上下文折叠丢错题、LLM忘记调工具、第1道题兜底失效……本文记录了每个问题的根因分析、解决方案,以及那些"Prompt写满也没用,必须代码兜底"的血泪经验。


前言:完成AI帮你出题、判题、给解析和自动记录错题这一套流程,怎么这么难?

做在线教育产品时,我设计了一个"考前测验"场景:用户做题,AI即时判对错、记录错题、自动生成薄弱点报告。

听起来很简单对吧?但开发过程中,我就遇到了这些问题:

“我做的错题Agent为什么没有自动记入错题库?”
“Agent说我第3题错了,为什么错题本里没有?”
“我的回答超出选项范围Agent怎么没发现?”

经过排查和重构,我修复了6个高优先级问题。这篇文章把踩坑和填坑的过程完整记录下来,希望能帮助到做类似功能的朋友。


痛点一:用户输入 F/E/G,AI 默认当作 D 处理

问题现象

用户在选择题中输入了 feg 等无效选项,AI Agent 并没有拒绝,而是直接当作 D 选项处理,甚至直接判对错。

根因分析

Prompt 层只写了"请核对答案",没有明确告诉 LLM:遇到非 A~D 的输入必须拒绝

LLM 的"善意揣测"导致它倾向于把用户的输入解释成有效选项,而不是质疑用户。

解决方案:双层拦截

第一层:Prompt 规则强化

exam-quiz.md 中新增输入校验规则:

输入校验规则:
- 用户必须输入单个字母 A/B/C/D
- 遇到非 A~D 的输入,必须明确提醒"选项范围仅为 A、B、C、D"
- 禁止猜测用户意图或替用户选择

第二层:代码层硬拦截

在 LLM 调用前,检测用户消息是否为单个非 A~D 字母:

stripped_answer = latest_user_query.strip()
if len(stripped_answer) == 1 and stripped_answer.lower() not in ('a', 'b', 'c', 'd'):
    if last_assistant_content and re.search(r'[A-D][.、.)\s]', last_assistant_content, re.IGNORECASE):
        # 直接返回提醒,不消耗 LLM Token
        return StreamingResponse(validation_warning_generator(), ...)

效果: 拦截后端直接返回 "选项范围仅为 A、B、C、D,请重新输入",完全不经过 LLM,零 Token 消耗。


痛点二:刷题多了上下文折叠,错题信息全丢了

问题现象

用户刷了 30 道题后,早期的错题信息在上下文折叠时丢失。最终生成的总结报告中,错题列表不完整,薄弱点分析也出现了偏差。

根因分析

通用消息折叠摘要只保留了"最近几轮"的内容,答题统计和错题清单没有结构化保留。

解决方案:测验专用折叠摘要

拆分检测逻辑,用不同阈值处理不同场景:

assistant_msgs = [m for m in request.messages if m.role == "assistant"]
quiz_pattern_count = 0
for am in assistant_msgs[-6:]:
    if re.search(r'[A-D][.、.)\s]', am.content, re.IGNORECASE):
        quiz_pattern_count += 1

is_quiz_mode   = quiz_pattern_count >= 2   # 折叠摘要用(需强证据)
is_likely_quiz = quiz_pattern_count >= 1   # 错题兜底用(第1题就生效)

折叠摘要时使用定制指令:

collapse_instruction = (
    "你正在总结一场**考前测验**的对话历史。请按以下结构生成摘要:\n"
    "1. **答题统计**:总答题数、正确/错误数量、正确率。\n"
    "2. **错题清单**:逐条列出每道错题(题目、用户答案、正确答案、知识点)。\n"
    "3. **薄弱知识点**:归纳学生反复出错的知识点。\n"
    "4. **关键诉求**:学生当前的核心需求。\n"
    "请务必保留每一道错题的具体信息,不可省略或合并。"
)

痛点三:工具调用消息刷屏,干扰用户体验

问题现象

错题入库时,前端显示一连串 "🛠️ 正在调用工具...""✅ 工具调用完成" 等系统消息,用户体验很差。

根因分析

tool_starttool_end 的 SSE 事件被前端直接渲染成文本。

解决方案

直接移除 chatStore.ts 中对 tool_starttool_end 事件的可见文本渲染。LLM 本身会在工具执行后给出自然语言确认(如"这道错题已帮你记录"),不需要额外的系统状态行。


痛点四:LLM 判错后"忘记"保存错题

问题现象

用户:“老师,我这道题选错了”
AI:“是的,你选错了,正确答案是 B”
(然后就没有然后了,错题没有入库)

根因分析

即使 Prompt 里写了"必须调用 save_mistake_records",LLM 也经常"忘记"。这不是 LLM 故意不听话,而是判错流程和保存流程分离后,执行链路断裂了。

解决方案:Prompt 强化 + 后端兜底双保险

Prompt 层:强制编号步骤

回答结构(每一步都必须执行):
1. 核对答案并告知对错
2. 立即调用 save_mistake_records 记录错题
⚠️ 步骤 2 不可跳过,必须在回复用户前完成

后端层:兜底机制(核心修复)

即使 LLM 完全忘记调工具,后端也能自动补救:

# 检测 LLM 回复中的错题语义(15+种模式)
wrong_answer_patterns = [
    r'不[太对]?对', r'[有错]误', r'不正确', r'有误',
    r'再想想', r'再考虑', r'误区', r'不是正确答案',
    ...
]
indicates_wrong = any(re.search(p, full_assistant_response) for p in wrong_answer_patterns)

# 检测本轮是否已调用 tool
has_mistake_tool = any("save_mistake_records" in str(r) for r in tool_records_to_save)

if indicates_wrong and not has_mistake_tool:
    # 触发兜底:轻量 LLM 提取错题 → 直接写库
    mistake_data = json.loads(extract_result)
    await save_mistake_records(subject=..., questions=[mistake_data], context=...)
    full_assistant_response += '\n\n(错题已自动记入错题本)'

痛点五:第 1 道错题永远不触发兜底

问题现象

用户只做了 1 道题就答错了,兜底机制没有生效,错题依然没有入库。

根因分析

测验模式检测阈值 quiz_pattern_count >= 2,第 1 道题答错时只有 1 条 assistant 消息含 ABCD,不满足条件。

解决方案:拆分阈值

is_quiz_mode   = quiz_pattern_count >= 2   # 折叠摘要用(需强证据)
is_likely_quiz = quiz_pattern_count >= 1   # 错题兜底用(第1题就生效)

低成本解法,在灵敏度和误判之间取得平衡。


痛点六:兜底成功入库后,AI 不知道

问题现象

后端兜底成功写入数据库了,但用户追问"我刚才那道错题你记了吗",AI 说"没有啊,我帮你记一下"——然后又重复入库。

根因分析

兜底消息只通过 SSE 发给了前端,没有追加到 full_assistant_response,所以没有存入数据库对话历史,LLM 下一轮看不到。

解决方案

# 关键修复:同时追加到 full_assistant_response
full_assistant_response += '\n\n(错题已自动记入错题本)'
yield f"data: {json.dumps({'content': fallback_note})}\n\n"

只有同时写入 full_assistant_response,这条消息才会进入对话历史,LLM 才能在后续交互中感知到。


经验总结:5条血泪换来的设计原则

1. 不要信任 LLM 的指令遵循能力

Prompt 里写"必须"、"强制"只能提高概率,不能保证 100%。关键操作必须有代码层兜底

设计原则:

Prompt 指令(第一道防线,概率性)
    ↓ 失败时
代码层兜底(第二道防线,确定性)

2. SSE 流中 yield ≠ 写入对话历史

循环外 yield 的内容虽然前端能看到,但不会自动进入数据库对话历史。如果要让 LLM 在下一轮感知到某条消息,必须同时追加到 full_assistant_response

3. 阈值设置需要考虑冷启动

>= 2 对"确认测验状态"合理,但对"第 1 题就触发"不够。拆分阈值是一个低成本解法。

4. LLM 有"学习"效应,但别被它骗了

用户反馈"第 2 题起 AI 就主动调工具了"——这不是 LLM 理解了规则,而是第 1 次手动提醒后,对话历史里有了 tool-call 先例,LLM 在做模式匹配。

不能在测试时误以为 Prompt 生效了,必须覆盖冷启动场景。

5. 沉默的兜底比没有兜底好,但透明的兜底更好

让兜底消息对 LLM 可见(写入对话历史),比只写日志强 100 倍。


效果图展示
进入考前测验模式
在这里插入图片描述
学生回答错误
在这里插入图片描述
错误的题自动记入,可在错题本中查看
在这里插入图片描述
结束测验,给学生测验报告
在这里插入图片描述
完整实现AI出题、判题、给解析、自动记录错题和总结测验报告一条龙服务。

结语

做 AI Tutor 系统,最难的不是"让 AI 回答正确",而是"让 AI 可靠地完成所有关联操作"。

LLM 的不确定性决定了:任何关键操作都不能依赖单一 Prompt 层,必须有代码层兜底。这是从"能用"到"可靠"的关键一步。

希望这篇文章对你有帮助。如果你在做类似的教育 AI 产品,欢迎交流!


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐