尧图精选

从零开发英语情景教学Agent:架构、Prompt与避坑实录

🕒 发布时间:2026/10/1 5:34:12 📁 来源:尧图网络
带过几期英语训练营之后我越来越觉得传统口语陪练产品的路子有个死穴剧本是写死的。学员在机场问路系统只会弹出“Excuse me, where is the gate?”——真实对话里的插话、口误、换说法它全都不接。所以我给自己定了个新目标从零到一开发一个英语情景教学Agent不是又一个聊天机器人而是能自己设计场景、控制对话难度、事后给反馈的智能陪练。这篇博客就是整个开发过程的完整复盘包括架构选型、Prompt设计、代码实现以及我在测试阶段踩过的那些坑。不管你是想给学生做口语陪练的老师还是在研究AI Agent落地的开发者这篇都值得看完。1. 为什么是Agent而不是普通App1.1 传统学英语的核心痛点传统英语学习软件最大的问题不在内容而在“交互模型”。一套课程做出来对话分支是有限的用户就算猜到最完美的答案也只是在预置的树状结构里走了一遍。真实的英语交流是什么样对方会打断你、换词、开新话题甚至故意装听不懂让你换个说法。这种动态性传统软件根本无法覆盖。我最早试过用关键词匹配来做情景对话用户说“book a room”系统就触发下一步。结果学员一旦说得稍微不标准比如“I want, um, to reserve a hotel room”关键词匹配直接就挂了。后来换成意图识别模型情况好一点但依然无法处理对话中的上下文连贯性——上一句还在问价格下一句突然问早餐时间传统NLU就把对话上下文丢了。1.2 Agent形态带来的本质变化Agent和传统规则系统最大的区别在于LLM具备理解复杂上下文、自主生成教学内容的能力。这个特性改变了三件事一是对话不再是“走剧本”而是“同一场景可以无限变化”。同一个机场问路场景Agent能根据用户英语水平动态调整语速、词汇难度和对话深度。初学者它多给提示词进阶用户它就故意制造信息差的干扰比如突然说“Sorry, the counter is temporarily closed”。二是反馈从“对错判断”变成了“能力面诊断”。传统系统只会告诉用户“正确/不正确”Agent可以把用户的口语输出拆解成流利度、语法准确性、词汇丰富度、交互策略四个维度还能针对每次的卡壳位置给出替代表达。三是教学策略可以实时演变。用户昨天的弱点是现在完成时Agent今天会在情景里故意多安排几个需要现在完成时的语境这是传统教学内容预设机制做不到的。1.3 这个项目适合哪些人参考如果你是教育类产品的产品经理想弄清楚Agent和传统应用程序的区别如果你是一个独立开发者想低成本验证一个AI应用的想法或者你是一个英语老师想让AI帮你做课后的情景陪练——这个项目都能给你一套可以直接上手的方案。我会把整个项目拆成五层需求设计、架构选型、Agent核心逻辑、前后端实现、迭代方向。这里面我不会刻意用花哨的Agent框架而是先带着大家用经典的大模型API写一套最小实现理解原理之后再看怎么往工程化方向演进。2. 整体架构设计与技术选型2.1 先画出数据闭环动工之前我给自己定了三个原则对话数据可回放、用户画像可累积、反馈结果可量化。围绕这三个原则整个系统的数据闭环是这样设计的用户语音 → 语音识别STT → 对话管理Agent → 情景策略调整 → 语音合成TTS → 用户 ↓ 对话记录持久化 → 能力评估模型 → 下一次情景定制核心数据流并不复杂复杂的是对话管理这一环。它不是简单地把用户的话丢给LLM拿回一个回复而是要结合当前情景剧本、用户长期画像、最近几轮对话状态决定纵容这段对话继续、插入教学提示、还是切换对话场景。2.2 LLM选型API优先还是本地模型优先这个选择题我纠结最久。市面上可选方案大致分三类方案类型代表优点缺点适用场景在线APIGPT-4o系列、通义千问、GLM系列、豆包大模型效果好、接入快成本随调用量增长、有网络依赖MVP阶段、中小教学场景本地小模型Qwen2.5 系列、Llama 3.1数据可控、单次成本低需要GPU、效果参差离线环境、数据敏感场景混合方案在线API做教学 本地模型做评估综合平衡架构复杂生产环境我的最终决定是MVP阶段直接用在线API原因很朴素——教学Agent的核心能力是“对话质量”对话质量取决于模型本身的理解能力。在模型效果无法保证的前提下花大量精力做Prompt优化和系统设计最终效果也不会好。等架构跑通了再考虑用蒸馏方案把高频场景压到本地小模型。这里有个小经验英语情景对话对模型的多轮上下文能力要求很高很多开源模型前三轮还行到第五六轮就开始重复提问或丢失角色约束。选型时不要只看榜单分数要做“十轮对话压力测试”。2.3 语音链路STT TTS的取舍英语口语教学Agent里语音链路比文字消息更重要。我试过三条路线第一条路线是云端串联模式。用讯飞或阿里的语音识别做STT识别结果给LLM处理LLM回复文本再交给TTS合成语音。优点是用到各家最成熟的模块每个环节都稳定。缺点是中间多了两跳网络延迟对话节奏会慢半拍。第二条路线是端到端实时语音模型。OpenAI的Realtime API把STT和LLM合成在一个模型里延迟很低但价格偏贵且只能配合特定客户端SDK。我评估后认为当前阶段不划算。第三条路线是第一轮识别用流式STT但把“确认——纠错”环节交给Agent本身。简单说不追求语音识别一次到位而是允许用户在语音输入后看到识别文本自己改一遍再发送。这个方案只有教育场景敢用因为学习者在对话里本来就是需要“看到自己的话被改过来”的。最终我的选择是第一版本先用本地WebSocket接入ASR准确率和速度平衡TTS使用Edge系列或OpenTTS类服务因为教学场景需要发音清晰、语速可调而且音色不能太机械。2.4 会话管理与双记忆体系情景教学Agent最本质的问题是要记住两件不同类型的事一件事是“当前这节情景课进行到哪了”。用户在A机场柜台问完航班现在想找登机口Agent必须知道这个问题属于当前情景的哪个阶段才能决定是直接回答还是接着扮演地勤人员。另一件事是“这个学习者往期的水平怎么样”。用户上次课里最容易犯的语法错误是什么昨天刚练过餐厅点餐今天就不该让他在咖啡厅场景里反复练同样的句型。为了解决这个问题我设计了双记忆体系用短期记忆存储当前会话状态用长期记忆持久化用户画像。短期记忆我直接塞在Prompt上下文里长期记忆则用结构化字段记录在数据库里。在架构演进阶段还可以把长期记忆升级成向量数据库检索但对口述项目来说结构化记录已经够用。3. 核心模块落地从Prompt工程到情景剧本3.1 Prompt设计让模型扮演角色而不出戏写Prompt前必须想清楚一个事情教学Agent不是“让模型回复正确答案”而是“让模型同时理解三件事——练习目标、剧情推进、用户意图识别”。我第一版Prompt写得一团糟把角色、规则、例子全部混在一段里结果是模型动不动就跳出角色变成百科问答。后来我总结出一个相对稳定的结构# 角色设定 你叫Karen是一名洛杉矶机场地勤人员。你在值机柜台工作了8年。 你会根据用户的英语水平调整语速和用词但绝不主动脱离角色。 # 教学指令 1. 每次对话前先判断用户的整句输出是否符合当前教学目标 2. 如果用户卡顿超过2秒给一个提示词不是答案 3. 如果用户说出与当前情景无关的话题礼貌地引导回情景 4. 如果用户出现高频语法错误如时态), 在对话中自然重复正确表达。 # 剧情进度控制当前节点3/5用户已办理值机下一节点询问登机口 # 若上一节点任务完成度高则在回答末尾给出半开放式追问 # 若任务完成度不足则主动重复上一节点核心话题。 # 输出格式 使用对话文本 辅助标签标签只给系统看包括 intent, skill_eval, should_end, difficulty_adjust关键奥秘在两处。第一剧情进度控制不要靠“请记住剧情”这种软性要求而是把状态直接结构化放在Prompt里每次对话后由代码更新再塞回去。第二skill_eval这种隐藏标签让模型输出的内容可以程序化解析我可以知道模型自己判断的教学意图。3.2 情景剧本的数据结构一开始我把情景剧本设计成纯粹的文本“场景是机场目标是练习现在完成时”。后来发现完全没法用因为没有节点控制模型第一轮可能就把整个流程走完了。我把它改成了基于节点的JSON结构{ scenario_id: airport_checkin_01, title: 机场值机——超重行李处理, target_skills: [conditional_sentences, polite_request], start_node: at_counter, nodes: [ { id: at_counter, role: agent, script: Good morning. May I see your passport and ticket?, expected_user_actions: [greeting, present_passport], follow_up_nodes: { direct: baggage_check, need_help: help_explain } }, { id: baggage_check, role: agent, script: Your suitcase is 24kg. The allowance is 23kg. A slight overweight., expected_user_actions: [ask_solution, pay_fee], follow_up_nodes: { direct: boarding_pass_issue, ask_exemption: explain_policy } } ] }这个结构的好处Agent在该节点时可以判断用户的response是否命中expected_user_actions据此选择下一步智能处理没有完全命中规则时的情况。我自己编写了一套简单的规则匹配器加上适合教学场景的意图识别。这套混合机制比纯规则灵活比纯LLM可控实际效果出乎意料地好。3.3 教学反馈的生成逻辑很多开发者会把“对话回复”和“学习反馈”混在一起这是大忌。对话过程中你应该让学习者沉浸在情景里误判用户的完整表达。一旦每个回合都插入“这里你该说过去式”体验就会变得像审讯一样。我把它们拆成两个通道对话回应用户面对面内容反馈在每个任务节点结束、或整段会话结束后输出。对话中 Agent: If your suitcase is overweight, you can either pay the extra fee or repack. What would you like to do? 对话后节点结束 feedback: { fluency: 72, grammar_hits: [Ive already bought two souvenir - Ive already bought two souvenirs], vocabulary_richness: medium, interaction_strategy: 基本能保持对话推进但主动提问偏少, suggestions: [ 尝试用What if...提出假设性问题, 把weight相关的同义表达练习一遍the scale, the limit, exceed ] }为了把流利度、覆盖率这些指标算出来我在后端写了一个打分模块综合参考用户自己的ASR识别结果、Agent记录的错误标志、用户整个会话中的字数占比得到量化的四维反馈。这个模块不能全信LLM输出一定要有自己的统计逻辑。4. 实操一个最小可运行的Agent4.1 环境准备与依赖安装我建议从头开始做一个小Demo不要上来就引一堆Agent框架。项目目录结构如下english-agent/ ├── agent/ │ ├── core.py # 对话核心逻辑 │ ├── scenario.py # 情景剧本加载 │ ├── memory.py # 双记忆管理 │ ├── feedback.py # 反馈生成 │ ├── stt.py # 语音识别封装 │ └── tts.py # 语音合成封装 ├── data/ │ └── scenarios/ │ └── airport.json ├── web/ │ └── app.py # 简单Web界面 └── requirements.txt运行环境我用的Python 3.10。依赖库尽量精简核心就三个大的openai或其他兼容OpenAI协议的SDKfastapi提供Web服务uvicorn启动服务。如果想做语音输入再加一个websocket库。创建虚拟环境并安装依赖python -m venv venv source venv/bin/activate pip install openai fastapi uvicorn pydantic python-multipart整个过程在Mac和Windows上都验证过Windows上如果遇到音频编码问题建议装FFmpeg并配置环境变量否则TTS合成的音频文件可能无法正常在浏览器里播放。4.2 核心代码对话状态机这里我先写一个基础版本用OpenAI兼容接口并演示如何将LLM回复、情景剧本节点、短期记忆三者结合起来完成整个对话流程。初始化部分from openai import OpenAI import json client OpenAI( api_keyyour_api_key, base_urlyour_compatible_endpoint ) class SceneAgent: def __init__(self, scenario_json): self.scenario json.load(open(scenario_json, encodingutf-8)) self.current_node_id self.scenario[start_node] self.short_memory [] self.user_profile {} self.finished False def _get_current_node(self): for node in self.scenario[nodes]: if node[id] self.current_node_id: return node return None核心生成逻辑def build_messages(self, user_text): node self._get_current_node() system_prompt f 你是一个英语情景教学Agent当前场景{self.scenario[title]} 你的身份是场景中的角色你正在与一个英语学习者对话。 当前情景节点ID{node[id]} 当前节点目标技能{self.scenario[target_skills]} 如果学习者表达困难可以用help标签提示但不要直接替他说完整句子。 如果学习者已达成节点目标回复末尾加上next标签。 messages [{role: system, content: system_prompt}] for m in self.short_memory[-8:]: messages.append(m) messages.append({role: user, content: user_text}) return messages def step(self, user_text): messages self.build_messages(user_text) resp client.chat.completions.create( modelyour-model, messagesmessages, temperature0.7 ) agent_text resp.choices[0].message.content self.short_memory.append({role: user, content: user_text}) self.short_memory.append({role: assistant, content: agent_text}) return agent_text这里有个细节short_memory只保留最近8轮超过的压缩成摘要。原因后面第5节细说。4.3 节点跳转与Completion逻辑光靠LLM自由发挥不行我用了一种轻量策略让模型输出特殊标记程序读取标记决定节点跳转。这不是把所有对话都交给状态机而是把对话的“驾驶权”保留在程序里。import re def step_with_control(self, user_text): node self._get_current_node() agent_text self.step(user_text) # 检测是否完成当前节点 if next in agent_text: follow_up node[follow_up_nodes][direct] self.current_node_id follow_up self.short_memory.append({role: assistant, content: 节点切换 }) return agent_text # 检测是否需要降级提示 if help in agent_text: return self._generate_help_hint() return agent_text为了实现“是否达成节点目标”的判断我在Prompt里要求模型在重新表达用户意图后给一次思考过程并输出{node_done: true/false}的JSON标签。程序解析标签再决定是否跳转。比如用户在前台说了一串很流利的请求但漏掉了“出示护照”这个关键动作模型会给node_done: falseAgent就会继续留在当前节点用不同的追问方式引导。这样一来模型负责理解和表达程序负责节奏掌控。4.4 语音链路的接入语音接入是整个Agent里最容易让人放弃的一环。我尝试了两种方式方式一浏览器录音 → 上传到后端 → 后端的ASR识别 → 识别文本传给Agent → 回复文本发到前端 → 前端用TTS接口合成。这种链路最适合轻量Demo延迟稍高但稳定。方式二浏览器里直接跑Whisper或其他模型的Web版本。这种方式省服务器资源但浏览器的麦克风权限策略、模型加载时间和不同浏览器的兼容性都很麻烦Mobile Safari上的表现更是灾难。我最终选了方式一因为教学场景对低延迟的容忍度其实比对准确率低很多。一个口语学习者哪怕你返回速度慢一秒他也不会太在意但如果识别结果错得离谱他会立刻对系统失去信任。STT段我直接参考了云厂商的流式语音识别接口。这里给一个后端伪代码结构async def transcribe_audio(audio_base64): # 这里调用你选择的ASR服务 result await asr_client.recognize( audio_dataaudio_base64, languageen-US, enable_intermediateTrue ) # 如果置信度低于0.7标记uncertain让Agent做提示 return { text: result.text, confidence: result.confidence }TTS我会选择支持SSML的引擎因为教学场景需要让系统输出更自然的停顿例如“Could you say it again differently?pausemsMaybe try I would like to...”。SSML里可控的pausems参数对学习者帮助非常大比单纯调整语速更有效。4.5 让Agent记住学习者Student Profile模块我用了比较轻量的个性化class StudentProfile: def __init__(self, user_id): self.user_id user_id # 重复错误例如 {past_tense_irregular: 3, singular_plural: 2} self.error_frequency {} self.common_level B1 self.interested_topics [] def record_errors(self, feedback_segment): # 遍历反馈里的grammar_hits统计错误类型 for e in feedback_segment[grammar_hits]: tag e[error_type] self.error_frequency[tag] self.error_frequency.get(tag, 0) 1有了档案Prompt的组装方式就要相应调整。如果用户高频错误是“可数名词单复数”在生成下一次对话时我会刻意让Agent在回复里多说复数句子让用户在听力输入里先建立感知。这个设计教学领域叫“input flood”。4.6 前端与可观察性前端我用的Streamlit不是因为它是最好的选择而是开发效率极高适合这类型的重交互原型。整体界面分三块左侧是对话聊天窗口像微信一样展示双方气泡。右上角是当前情景节点指示器显示“值机柜台/行李检查/登机牌打印”。右下角是实时技能诊断面板展示每个节点的评分情况。实际操作中我发现学习者最关注“我刚说的这句话错误在哪”。所以我在聊天窗口里把每句话的ASR识别文本显示出来让用户看到系统理解的内容是什么。这个设计虽然简单但对口语教学特别关键——用户需要知道自己说得对不对才有方向改进。5. 常见问题与排查技巧实录5.1 LLM角色越界一不留神就开始科普这几乎是所有Agent项目的通病。我第一版测试时用户只说了句“I am tired”我的Agent立刻跳出情景开始长篇大论科普睡眠的好处看得我哭笑不得。排查思路是这样的先把Prompt的role设定从“普通描述”改为“硬性命令”。比如从“你是一个地勤人员”改成“你必须始终扮演地勤人员。除非用户明确说结束情景否则任何新话题都由你用地勤人员的口吻重新引导到机场场景”。加了“必须”和“否则”之后效果立刻改善了很多。如果还是不听话就在推理参数上动手OpenAI客户端里temperature从0.8降到0.3但这会导致对话变得无聊、重复。我更推荐的是在请求时加response_format之类的约束让模型输出必须带情景标签这样角色不贴合的几率就低很多。5.2 剧情推进太快用户一句话模型把整个戏演完脚本的下一个节点还没到模型就已经把登机牌给用户打好了。这是我在情景Agent里遇到的第二个高发bug。解决方案是把剧情的“进度变量”彻底从Prompt里抽离改用程序变量。只要模型看到current_node_id是at_counter它就只能围绕这个节点展开。即使它想推进剧情我这边只要检测到它生成了过于超前的信息就用一个过滤函数把回复拦截下来def guard_node_response(node, agent_response, next_node): # 如果回复中出现了下一个节点才应出现的实体词则禁止触发next disallowed_terms next_node[script_entities] for term in disallowed_terms: if term in agent_response: return agent_response.replace(next, ) return agent_response这个Guard策略很土但真实项目里它比任何复杂方案都稳定。依托规则判断语义比依赖大模型自律更可控。5.3 记忆膨胀对话越来越慢钱越烧越快跑了20轮的对话如果每次请求把全部历史都塞进PromptToken消耗越来越高回答延迟也肉眼可见变长。这是所有Agent的共性问题。解决方法我试过两种滑窗摘要和关键记忆抽取。对话进行到第10轮时做一次中间摘要 “用户已经完成值机正在询问行李超重问题他的回答偶尔出现助动词遗漏。” 摘要存在short_memory里后续请求只带摘要 最近6轮原始对话。长期记忆则单独存在StudentProfile里按“错误频次兴趣标签”存结构化字段每次请求只带Top3相关记忆。这样即使在长对话中Prompt也能保持稳定。5.4 并发不高先别背框架的锅很多开发者会在一开始考虑“我的Agent能扛多少并发”实际是典型的过度设计。我在测试阶段用FastAPI异步接口单机跑实测300个并发请求时LLM API的响应就成了瓶颈——但这不影响Agent本身因为真正耗时的环节都在外部API调用上。如果你真的要在生产环境扛并发我的建议是给LLM调用加一层异步队列而不是把整个对话逻辑写得花里胡哨。用一个简单的内存队列或Redis队列做削峰配合超时重试机制效果比调整Python代码快得多。还有一个容易被忽略的点语音识别和TTS的高并发需要大量的音频转码资源。我建议把音频统一转成PCM或WAV 16k单声道可以省下不少转码时间。音频格式不对导致ASR接口报错的问题我在测试阶段遇到过不下五次。5.5 用户乱说无关话题怎么办情景教学Agent永远会遇到学习者脱离剧本的情况。我一开始的应对是用规则拦截“请回到机场场景”。结果学员体验感很差那种感觉像被老师点名拉回座位上。后来我把方案改成“先接住再自然拉回”。比如用户在机场柜台问“Can I bring my dog with me?”Agent会先以航空地勤的身份回答宠物运输政策再反问一句“By the way, do you need to check your suitcase first?”这样既保留了对话的开放性又没有让情景崩塌。也就是说模型输出的内容就是自然的对话延伸即使超出原剧本范围也是合法的只要角色没有脱离场景即可。6. 效果评估什么才算“有效教学”6.1 主观体验必须量化Agent上线后我最先做的是找14位不同水平的用户测试。每位用户跑3个场景每个场景约10分钟。测试后我会收集两层数据主观满意度问卷和客观表现评分。客观评分这个环节我会记录用户在对话里的平均字数、每分钟有效输出字数、语法错误频率、主动提问次数。这里有个有意思的发现Agent是否给出“跟随式追问”直接决定用户的开口时长。当Agent回复末尾附带上一个半开放问题时用户下一轮的字数平均会增加三分之一。另一个经验是不要让Agent太“爱纠正”。我本来设计在每个节点结束后都给一份详细反馈但实际使用中如果每个节点都大段反馈用户根本读不进去。后来改成只对“错误频率最高”的1-2个点进行深度点评反而更有效。6.2 Agent教学能力的边界做了这么多轮测试我想说句实在话Agent只能做口语陪练不能替代真正的老师。原因在于LLM的结构性缺陷——它对真实语言学习的策略性把握还不够。比如一个初学者连最基本的“Where is the restroom”都说不利索Agent可能会给他安排一个机场转机的情景完全没有意识到这个场景对他来说过难。要让Agent有“教学定级”的能力还得靠外部条件。我的方案是加入一个规则引擎根据用户历史成绩和本次节点的平均表现自动控制节点的“难度挡位”。难度挡位 词汇复杂度 语速 干扰项数量 如果用户最近3场均分 60难度挡位 基础 如果用户最近3场均分 60-80难度挡位 中等 如果用户最近3场均分 80难度挡位 进阶此时加入时间压力和口语中的不确定信息6.3 这个项目以后还可以怎么改目前这个版本的核心五脏俱全但还有三个值得扩展的方向。方向一是引入“意图不确定性”判断。现在ASR置信度低时Agent会直接把识别文本发给LLM而LLM会根据错误文本生成答非所问的回复。正确做法是当ASR置信度低时走专门的“澄清对话流程”让用户重说一遍或换个说法。方向二是把反馈从“文本报告”推进到“点击交互”。用户看到自己说错的地方可以直接点击录音重说一遍系统对比新旧输出测出用户目前发音的准确度。方向三是在教学情景里引入多模态素材。比如用户学“预订机票”Agent可以推送一个真实的航空邮件或登机牌截图让用户在认读真实材料的过程中完成练习。这次开发我收获最大的一点不是Agent技术本身而是它让我对“教学交互”有了新的理解真正的学习反馈不是用户哪里错了而是用户离“能自主表达”还缺哪一步。Agent要做的不是代替老师回答而是替老师观察每一次“卡住”的瞬间然后用正确的追问帮学习者自己跨过去。如果你也打算做一个情景类Agent我的建议很简单先把一个场景做透做到你愿意拿它面对真实用户再谈框架和扩展。这个过程中你会踩的坑基本跟我上面写的差不多。希望这篇能让你少走几步弯路。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →