☰
英语情景教学Agent:从LangGraph状态机到纠错反馈的工程实践
2026/9/29 5:43:01 网站建设 项目流程

1. 项目定位:这个英语情景教学Agent到底解决什么问题

1.1 用户真正缺的不是词汇,而是"在场景里开口"的经验

我一直觉得,市面上的英语学习产品大多在解决"记住"的问题,而很少解决"敢说"和"会说"的问题。背单词、刷语法题、看美剧学英语,本质上都是在积累知识,但一到真实的对话场景——出国点餐、入住酒店、参加英文面试——大脑就一片空白。这是因为语言知识从"认识"到"运用",中间还隔着一个叫"情景反应"的环节。

英语情景教学Agent想做的事情,就是把这个环节补上。它模拟一个具体的场景,Agent扮演对话中的另一方,比如前台接待员、咖啡店店员、面试官,用户通过和它对话完成一次情景演练。和普通ChatBot最大区别在于:普通ChatBot的目标是"回答用户的问题",而这个Agent的核心目标是"让用户完成一次有教学目标的对话练习"。

所以这个项目从一开始就不能套用"投喂一个System Prompt然后聊天"的简单模式。情景教学对Agent有三层要求:第一,必须限制在场景内,不能聊着聊着跑题变成闲聊;第二,必须能够给出教学反馈,而不只是对话回应;第三,需要根据学习者的水平动态调整对话难度。这三层要求直接决定了后面所有的架构设计。

1.2 MVP功能边界:设定目标时先学会做减法

做这个项目之前我列了个很长的愿望清单:发音评分、动态生成情景、多Agent模拟群聊、VR沉浸式……后来全部划掉了。一个从零到一的教学Agent,第一版最该做的是验证"情景对话+教学反馈"这个核心闭环到底能不能跑通。

我的MVP最终只保留了四个功能:

  • 预设情景脚本:先内置10个高频生活场景,比如机场值机、咖啡馆点单、酒店入住、英文面试
  • 角色扮演对话:Agent按场景脚本扮演指定角色,用户自由输入回答
  • 实时纠错反馈:每轮对话结束后,Agent对表达中的明显语法错误和用词问题进行反馈
  • 学习档案记录:记录用户每轮表现,生成阶段性薄弱点小结

刻意砍掉发音评分。原因很简单:发音评分牵涉到ASR的声学特征分析,需要单独引入音频层面的模型,和语言教学Agent的逻辑不属于同一条技术线。强行塞进MVP,会在联调阶段消耗大量精力。先集中精力把文本对话闭环做扎实,发音能力放到后续版本通过独立模块接入。

关于功能边界还有一个容易被忽略的点:不要把Agent设计成"全知全能的自由对话者"。在情景教学场景里,Agent的自由度越高,教学失控的风险越大。用户说一句"我昨天看了一部电影",如果Agent顺着他聊起电影剧情,这堂"机场值机练习课"就跑偏了。所以第一版的Agent必须是有边界的、有剧本约束的,这个设计思路贯穿了后面的整个技术方案。

2. 技术选型:别一上来就堆多Agent编排

2.1 框架对比:LangChain、LangGraph与最小自研方案

"要写Agent"的时候,很多人第一反应是选一个Agent框架,把这个词用上去。但是我个人的建议是:先确定你的应用到底需不需要那种"多Agent自主规划、相互调用"的重型架构。英语情景教学本质上是一个强流程、弱规划的应用——对话阶段是确定的,教学动作是确定的,真正需要LLM发挥的是"生成符合场景的表达"和"判断表达质量",而不是让AI自行规划学习路径。

我对比过三条路线:

方案状态管理能力学习成本可控性适合场景
LangChain(经典链式)弱,靠外部变量拼接低中简单问答、文档流程
LangGraph(图编排)强,原生支持状态节点流转中高高有明确多轮对话状态机的应用
自研轻量引擎完全自己控制低(不依赖复杂API)最高状态简单、逻辑明确的应用

最终我选择了LangGraph作为主框架,但没有把整条链路都交给它。LangGraph在"对话状态流转"这件事上做得非常合适:每个对话阶段是一个节点,用户输入触发一次状态迁移,迁移前后可以附加数据处理逻辑。这和情景教学Agent天然一一对应——"正在询问航班号"是一个阶段,"用户提供了错误答案需要纠偏"是另一个阶段。

有人问为什么不干脆自研?我的判断是:这个项目里状态节点有十几个,但如果自己实现中断续执行、状态持久化和回滚,工作量会明显增加。LangGraph把这些能力内置了,我只需要关注业务逻辑。

2.2 单Agent核心链路:先跑通完整数据流

很多人被"多Agent协作"这个概念吸引,觉得多个Agent角色扮演一个老师一个学生才够高级。实际开发中我建议第一版坚持单Agent核心链路。原因很简单:多Agent的通信开销和一致性维护成本,在MVP阶段会拖慢迭代速度。

我的核心数据流是这样的:

用户语音 -> STT转写 -> 情景状态机判断当前阶段 -> 组装当前情景信息 -> LLM生成Agent回应与反馈 -> 解析结构化输出 -> 更新状态机 -> TTS播放(可选)

这个链路里真正调用LLM的环节只有一个:根据当前状态生成教学回应。前期先把这一条链路打磨好,等数据积累够了,再去考虑要不要拆一个"出题Agent"和一个"点评Agent"。

有个细节值得单独说:LLM的输出不要直接当最终结果。我会要求LLM返回固定格式的JSON,包含"reply"(情景角色说的话)、"correction"(如果有错误需要纠正)、"error_type"(错误类型)等字段。这样状态机可以稳定解析并决定下一步动作。很多Agent项目跑飞,就是因为把LLM的自由文本直接丢给逻辑判断,最后正则都救不回来。

3. 情景引擎:把真实对话场景变成可执行脚本

3.1 情景脚本的数据结构:让"教学剧本"不再靠Prompt硬撑

这是整个项目里我最想强调的部分。很多教程教人做Agent时,会把场景信息全部塞进System Prompt,比如"你是前台接待员,用户正在办理入住"。这种方式Demo阶段跑得很欢,但一旦场景变多、分支变复杂,Prompt会越来越臃肿,LLM的发挥越来越不稳定,而且教学知识点根本没法结构化统计。

我的做法是为每个情景设计一个独立的JSON脚本,作为整套系统的"剧本"。

{ "scene_id": "airport_checkin_001", "title": "机场值机英文实战", "target_level": "intermediate", "roles": { "agent": "地勤工作人员", "user": "办理值机的乘客" }, "objectives": ["询问航班号", "确认座位偏好", "托运行李相关表达"], "key_points": { "vocabulary": ["boarding pass", "window seat", "check-in baggage"], "grammar": ["Could I...", "I'd like to..."] }, "stages": [ { "id": "greeting", "description": "地勤主动打招呼并询问航班号", "agent_action": "greet_and_ask_flight", "expected_user_behavior": "说出航班号", "next_stage": "seat_preference", "error_prompt": "如果你不确定航班号怎么说,可以这样表达:My flight number is..." }, { "id": "seat_preference", "description": "询问座位偏好", "agent_action": "ask_seat_preference", "expected_user_behavior": "表达靠窗或靠过道等偏好", "next_stage": "baggage_check" } ] }

这个脚本结构的核心价值在于:教学目标和对话流程不再是隐性的,而是显式的数据。系统可以随时知道当前要练习什么知识点、用户应该给出什么类型的回应、如果卡住了应该给什么提示。这也让后续的评测和数据分析有了抓手——我可以统计用户在哪一个stage反复卡壳,从而知道他的薄弱环节。

3.2 对话状态机:防止Agent跑偏的保险绳

用脚本定义了"应该发生什么",还需要一个状态机控制"实际发生什么"。我的实现思路是:LLM的每一轮输出,不是从零生成回应,而是在当前stage规定的范围内生成。

状态机维护了几个核心字段:

  • current_stage:当前对话阶段ID
  • stage_history:已经走过的阶段列表
  • user_errors:本场景内用户出现的错误记录
  • hint_count:当前阶段已经给出提示的次数

每一轮处理大致是这样的逻辑:用户回复进入后,先做一次轻量级的意图判断——判断用户是否完成了当前阶段的目标。完成则推进到下一个stage;没完成则留在当前stage,并决定是给提示还是给纠错。这个意图判断任务,我仍然交给LLM,但要求它只输出一个结构化的判断结果:

from typing import Literal def decide_next_step( user_input: str, current_stage: dict, llm_client ) -> dict: prompt = f""" 你是英语教学系统里的对话策略模块。以下是当前教学情景的对话阶段信息。 阶段ID:{current_stage['id']} 阶段目标:{current_stage['expected_user_behavior']} 用户刚刚说:{user_input} 请判断: 1. 用户是否已经完成了本阶段的交流目标?YES 或 NO 2. 如果未完成,主要原因是:A. 理解不了场景 B. 表达不完整 C. 词汇/语法错误 D. 沉默或跑题 3. 下一步动作:ADVANCE(推进下一阶段)/ STAY(留在本阶段)/ HINT(给提示)/ CORRECT(纠错) 输出JSON格式,不要有额外解释。 """ raw = llm_client.chat(prompt) # 这里做解析和schema校验,失败则默认STAY return parse_json_with_schema(raw)

这段代码里有个容易被忽略的设计:如果LLM返回的JSON解析失败,默认动作是STAY而不是报错。英语对话场景里,用户输入口语化、不完整、带噪音的概率很高,加上LLM本身输出偶尔会不守规矩,兜底策略一定要稳。宁可让对话原地多转一轮,也不能让状态机崩掉。

3.3 阶段推进逻辑:一个"安全阀"设计

状态机的另一个关键设计是融入了"教学干预"的机制。如果用户在同一个阶段连续触发Hint超过两次,System Prompt会切换角色模式——Agent从"情景角色"切换到"陪练教练",主动给出示范表达,让用户跟读并复述。这样做是为了防止用户在一个点上卡太久产生挫败感。

这个机制被放在状态机层面而不是放在LLM的System Prompt里,好处是逻辑可控。不然你写一万句"如果用户不会表达就耐心示范",LLM也很难每次稳定做到。从工程角度讲:能放到代码里的确定性逻辑,就别交给模型去赌概率。

4. 记忆系统:短期上下文与长期用户画像的落地方案

4.1 短期记忆:既要上下文连贯,又要控制Token成本

英语情景对话有一个特点:每轮对话的用户输入普遍很短——"Yes"、"Window seat, please"、"I have one suitcase"。但整个场景对话可能持续15到20轮,如果把所有对话历史全塞进Prompt,会让Agent对最近的内容失去敏感度,还会显著增加成本和延迟。

我的短期记忆方案是双轨制:

  • 结构化状态:状态机里维护的阶段信息、错误记录、当前目标,这些是"机器能理解的记忆"
  • 原始对话缓冲:保留最近四轮用户输入和Agent回应,作为"模型需要的上下文"

关键点是:结构化状态是所有记忆的核心,原始对话只是辅助信息。因为状态机通常只需要知道"用户当前在哪个阶段""刚才的错误类型是时态还是词汇",就足够生成合适的回应了。原始对话留四轮,是为了保证对话衔接自然——毕竟用户上一句说了"Actually I prefer aisle seat",下一句Agent不能假装没听到。

场景结束后,短期记忆会做一次摘要压缩并归档,然后清空缓冲。这里有个教训:一开始我把摘要直接喂回下一场场景的Prompt,导致Agent出现"记忆穿越",在前台场景问用户"您的行李箱需要托运吗"——这是上一场机场场景的聊天内容。后来统一改为每次场景切换只保留用户档案,不保留对话摘要,问题彻底解决。

4.2 长期记忆:用"学习档案"而不是无限堆积历史

长期记忆是Agent类项目里话题度极高的概念,"有哪些记忆类型""怎么实现永久记忆"之类的讨论很多。就英语情景教学场景来说,长期记忆不需要记录用户说过什么原文,比如三周前他说过"I go to school by bus",这个原文没有长期保存价值;有长期价值的是由此抽取出的结论:"一般现在时第三人称单数容易漏加s"。

我把长期记忆设计成一份结构化的用户学习档案:

字段示例更新策略
常错知识点"一般现在时三单"连续两个场景出现同类错误时记录
已掌握知识点"can 情态动词用法"连续三个场景正确使用后标记
偏好场景"英语面试"用户主动选择频率统计
难度等级intermediate正确率和反馈推进速度综合计算
敏感反馈"用户不习惯被连续纠正"用户反馈问卷获得

档案的更新不能实时进行。如果每轮对话都更新档案,系统会显得"人格漂移"——这轮用户犯了个低级错误,难度等级立刻下调,下一轮Agent的对话难度突然变得过于简单,用户体验很差。我的做法是:用户档案只在每个情景结束后异步更新。对话过程中的所有表现先记录到临时列表,场景结束后统一分析、更新档案。这样既保证教学策略稳定,也不会拖慢实时对话响应。

4.3 记忆安全:教学数据也有一个"最小化原则"

Agent记忆越丰富,教学越个性化,但代价是隐私风险。英语对话天然包含大量个人信息——用户可能会在练习中说自己的航班号、家庭住址、工作单位。我在设计记忆模块时给自己定了几条规矩:

  • 用户原始对话不持久化,只保留抽取后的教学特征
  • 用户档案中不存姓名、电话、地址等身份信息
  • Agent在对话中收到真实个人信息时,自动将角色回应拉回教学主线,不追问细节

比如用户在练习酒店入住时随口说"我的护照号是E12345678",Agent的回应应该礼貌承接然后转向教学任务,而不是表现出对护照号本身有任何兴趣。这些限制不是靠开发人员自觉,而是在状态机层面做了模式匹配拦截,并在训练提示里明确说明。

5. 纠错与反馈:教学环节的"分寸感"最不好调

5.1 纠错时机和粒度:每轮都打断是教学灾难

做教学Agent很容易陷入一个误区:把LLM当成语法检查器,用户每说一句话都要纠正一遍。试想一下,如果你说一句"Can I get a window seat",Agent立刻回一句"注意,此处应该用Could I,更正式",而且每轮都这样——用户练五分钟就不想练了。

我最终采用的纠错策略是两层:

第一层,即时纠错。只处理那些"导致理解受阻"或"与当前教学目标强相关"的错误。比如用户说"I no have luggage",这个句子很可能让Agent角色(值机柜员)理解困难,必须即时纠正。再比如当前教学目标恰好是"过去式",用户说"I go to London yesterday",这个错误直接攻击教学目标,也即时纠正。

第二层,回合末总结。把那些不影响理解的、偏表达优化的建议(比如"试着用Could I替换Can I,会显得更礼貌")放到整个场景对话结束后统一给出。

{ "reply": "Sure, your flight is on time. Do you have any baggage to check in?", "correction": { "triggered": false } }

那么"是否触发即时纠错",怎么判断?前面提到的策略判断模块返回的next_step就是依据。如果next_step是CORRECT,则走即时纠错分支;否则Agent的回应里不夹带纠错内容。这样纠错不再是一刀切,而是有策略的、伺机而动。

5.2 防止两个让用户崩溃的反馈风格

我在跑通初版demo后做了个小范围试用,暴露出两个高频问题。正好这两个问题都有对应的调优经验。

第一个问题是AI过度表扬。几乎所有轮次都以"Great!"、"Perfect!"开头,哪怕用户说得明显不自然。一开始觉得这是"鼓励式教育",但用户反馈却说"感觉像敷衍,不认真"。后来我调整了提示词:只在用户确实完成阶段目标时给简短肯定,普通回应直接给出自然的角色反应。同时加入了"肯定等级"控制:great / good / well done / nice try,让LLM按用户表现动态选择。

第二个问题是AI夹带中文。模型在生成纠错内容时,偶尔会蹦出"你这句话说得不错,但注意这里要用过去时"——这个"注意"倒没什么,问题是它可能会用一整段中文来解释语法。对英语学习者来说,某些情境下中文解释是有用的,但既然定位是"情景沉浸式",我会在生成回复的提示里显式约束:回复正文和点评文字默认使用英文,除非用户的主动提问使用中文。这个约束用few-shot示例比单纯强调更有效。

示例1: 用户输入:I have been to Beijing last year. Agent角色回应:You've been to Beijing? When did you go there? 点评:注意"last year"是明确的过去时间词,用一般过去时更合适,比如 I went to Beijing last year. 示例2: 用户输入:不好意思 我不太会表达 Agent角色回应:No worries. You can say: "I'd like to book a single room."

第二个示例非常关键——用户用中文求助,Agent可以允许中文,但必须同时给出英文表达,并且要给出"在这个场景下马上能用"的表达,而不是一个冷冰冰的语法规则。教学导向要时刻体现在Agent的输出里。

5.3 Prompt的温度参数:教学场景不能太"随意"

还有一个细节很多人不会注意:参数设置。情景对话Agent我建议temperature设置在0.7左右,而纠错点评模块单独设置更低的值,比如0.2到0.3。原因是情景角色对话需要一点自然变化和灵活性,但纠错点评追求的是一致性和准确性。同一个LLM、同一个项目,不同模块手滑用一个参数,实际效果差的不是一点半点。这种模块级的参数拆分,是从传统聊天机器人转向教育产品时必须补的一课。

6. 从能跑到好用:联调、评测与真实场景打磨

6.1 搭建离线评测集:不要靠肉眼感觉验收

Agent类应用最让人头疼的是"感觉好像还行"——自己测对话轮次时觉得挺自然,但交给别人用就不是那么回事了。所以我从开发中后期开始,搭建了一个小型的离线评测集。

评测集结构不复杂,每个条目包含:

  • 情景ID(比如airport_checkin_001)
  • 用户输入(尽量覆盖口语化的、有错误的、跑题的输入)
  • 当前阶段ID
  • 期望策略动作(ADVANCE / STAY / HINT / CORRECT)
  • 期望Agent回应里必须出现的知识点或词汇

我会用LLM作为裁判来跑自动测评,按几个指标统计:

指标含义合格线
策略准确率next_step判断是否与预期一致90%以上
知识点覆盖率单场景内教学目标关键词在Agent回应中出现的比例80%以上
中文串扰率不应出现中文的回复中是否夹带中文低于5%
无效回应率是否出现答非所问、逻辑断裂的回应低于5%

自动测评通过之后,再找人做人工体验,重点看的是"教学感"而不是"对话感"——也就是用户练完这个场景,有没有感觉到自己学到了东西。自动指标和人工感受经常出现偏差,比如一次对话里纠错频率太高,指标全绿但体验分很低。这些只有靠真实用户反馈才能发现问题。

6.2 我在实际开发中踩过的几个典型坑

很多问题是在真实联调时浮出来的,我在这里记一下,省得后面有人重复踩。

第一个坑是状态机没有超时和熔断机制。之前设计的状态机只是"等待用户输入->处理->状态迁移",但用户可能中途不说话了、直接乱打一通、或者网断了重连。这些情况不做处理,状态机会卡在某个阶段不出来,用户再回来时体验非常差。解决办法是在状态机外再加了会话级心跳检测:如果一段时间没有输入,就主动用Agent的话术引导用户继续,比如"Are you still there? Would you like to continue the practice?"

第二个坑是ASR的错误文本被大模型当成正规英语来教学。语音转写口语文本往往没有标点、带语气词、有识别错误,比如用户说"Can I get a window sit",ASR可能转成"Can I get a window seat",结果模型以为用户说对了。这个问题的根治很麻烦,短期方案是在LLM生成求判断之前先拼接一段提示:"以下是语音转写结果,可能存在识别偏差,请结合上下文判断,不要因为转写错误而误判用户水平。"这只一个缓解手段,但实测能有效降低误判率。

第三个坑是用户档案的误更新导致教学行为突变。有一次系统在用户连续失误后把难度等级从"intermediate"调到"beginner",结果接下来的整场面试练习变成了蹦单词式的一问一答,用户被当成了完全零基础。问题就出在前面说的"场景结束后异步更新"的策略被临时调试代码破坏了——有个试验开关把实时更新打开了。此后我再不敢在线上随便改记忆模块的更新策略。测试可以开灰度,但不能拿一套逻辑同时跑两种模式。

6.3 后续可以扩展的几个方向

这个项目目前跑通的是"预设脚本+单Agent教学闭环",但如果继续往下做,我认为有三个明确方向。一个是动态情景生成,根据用户的档案和学习目标,让Agent自己设计新的场景脚本,这一步会真正用到规划能力。第二个是多角色扩展,比如面试场景里除了面试官,再加入一个"观察者"角色,在面试结束后给出点评——这是比较自然的多Agent使用方式,而不是为了用而用。第三个是把分析能力做成课后报告,让用户看到自己这个场景里错误类型的分布趋势、词汇缺口和进步轨迹。

不过这些扩展都有一个共同前提:先把现状的基础评估跑明白。教学类应用最忌讳的就是无限堆功能,结果核心体验却没做好。一个新场景上线之前,至少要跑过十轮真实用户对话,把策略准确率、知识点覆盖率这些指标都过一遍,确认不会出现明显的地质性崩坏,再放量测试。这个流程比我一开始想象中的评测复杂度要高不少,但对教学效果的把控非常值得。

从我个人的开发体验来看,英语情景教学Agent并不是一个单纯"调大模型"的项目。它的难点在于把教育学的分寸感转化成工程上的确定性逻辑——什么时候推进、什么时候纠偏、什么时候给提示、什么时候鼓励,每一个决策节点都要能被代码控制、被数据验证。也恰恰是这部分,才是我做完之后觉得最有价值、最值得拿出来分享的地方。如果这个项目能给你一些启发,那大概就是同一个思路:Agent不是越自由越好,越贴合场景、越有边界,才越有可能成为真正可用的产品。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询