做这个项目的起因特别简单我发现很多学员背单词能背到 CET 六级但进了咖啡厅点单还是只会说 “this one, this one”。我本来就在做 AI Agent 开发当时就在琢磨能不能用大语言模型的能力做一个“陪练教练”让它扮演咖啡店店员、机场地勤、酒店前台把真实对话场景直接推到用户面前。这篇文章记录我从零到一开发这个英语情景教学 Agent 的完整过程需求拆分、技术选型、Prompt 设计、语音链路搭建、代码骨架以及我踩过的坑。想入门 Agent 开发的朋友可以参考想做语言教学产品的朋友更能抄走不少思路。说实话这个项目比我想象中更难的地方不在大模型调用而在“怎么让一个聊天机器人像真人一样陪你练口语并且给出恰到好处的反馈”。很多现成的 Agent 框架一上来就给你一堆编排、记忆、工具调用的抽象但对“英语情景教学”这种垂直场景来说最有价值的部分其实是场景状态管理和反馈策略。下面按我的实际开发顺序把整个过程拆开讲。1. 需求分析英语情景教学 Agent 要解决的三个核心问题1.1 传统英语口语练习的痛点先别急着写代码把用户问题想清楚比什么都重要。我调研了身边的自学用户、备考雅思的同学还有准备出国旅游的人发现三个高频痛点。第一缺少语境。大多数人背单词是“单词—中文释义”的映射完全没有“这个单词在真实场景里怎么接话”的概念。第二缺少即时反馈。自己跟着 App 读读对读错没人告诉你吞音、重音错位自己根本听不出来。第三缺少开口压力。真人一对一老师贵AI 对话没有心理负担用户反而更愿意开口。这三个痛点直接决定了 Agent 的产品形态它必须是一个“模拟真实对话”的角色而不是一个“语法讲解员”。1.2 场景范围与用户画像做产品最怕贪多。第一版我只锁定了三类高频场景机场、咖啡厅点单、酒店入住。为什么选这三个因为它们流程固定、话术重复率高、用户出门就能用非常适合做状态机结构。再往后扩展餐厅、购物、问路都顺理成章。目标用户是三类人备考雅思口语的考生需要高频但低成本的陪练准备出国旅行的游客需要短时间内补上生存口语还有职场人想练英语但不想暴露在真人面前。所以 Agent 的核心定位不是替代老师而是提供一个“什么错都不丢脸”的高频练习环境。1.3 功能需求优先级排序需求必须分级否则开发周期会拉得无限长。P0 是必须有文字对话、角色扮演、简单评分、对话历史记录。P1 是应该有语音输入、语音合成、场景状态推进、常见错误解析。P2 是可以以后再做真人语音克隆、通话级低延迟、接入雅思题库、多语言教学。我的经验是第一版先把 P0 和 P1 的核心路径打通即使语音延迟做不到实时先做成“按住说话—等待—听回复”的异步模式也完全够用。很多 Agent 项目死于一开始就追求复杂编排和记忆最后连一个最基础的对话闭环都没跑通这个顺序千万别搞反。2. 技术架构设计与 Agent 框架选型2.1 Agent 的四个基本组件我理解的 Agent 不是“调一个 API 聊聊天”而是有一套自己的执行回路感知输入、规划动作、调用工具、记忆上下文。放到英语教学场景里就是用户说了什么感知— 判定现在处于哪个剧情节点、是否要纠错规划— 查词典或生成长对话回复动作— 记下用户说错的地方和当前话题记忆。这个分类非常重要因为它决定了我选型时该关注哪些能力。很多 Agent 框架把 Planning 和 Tools 做得花里胡哨但在这个项目里真正的核心其实是“记忆”和“状态”因为教学 Agent 必须知道用户上一轮犯了什么错这轮该怎么练。2.2 轻量架构 VS 重量级 Agent 框架当时摆在我面前的有两条路。一条是直接用 LangChain、AgentGPT 这种重量级框架好处是编排、记忆、插件生态现成坏处是抽象层太厚出了问题很难定位。另一条是自己用 FastAPI OpenAI 兼容接口搭一个轻量服务把 Agent 逻辑写成状态机。我最终选择了轻量路线。原因是我发现现有框架对“结构化输出”和“状态强约束”的支持并不好而教学场景最吃这两点。我需要模型每次回复不仅给用户回答还附上评分、纠错提示、情绪激励、下一轮引导这个需求用 JSON Schema 约束让模型输出结构化结果比自己硬套框架更可控。当然这基于我的常见实践如果你要做一个通用型 Agent框架依然值得考虑。2.3 语音链路选型STT 与 TTS语音输入我用的是 OpenAI Whisper 的本地部署也可以用云端 API因为它在噪杂环境下识别英文准确率比大部分实时转写服务高。输出的语音合成我用了 Edge-TTS免费且支持多发音人口语教学已经够用。建议不要一开始就上 WebRTC 实时通话那样要处理回声消除、半双工、音频缓存调试成本会急剧上升。最终的技术栈是Python 3.11 FastAPI SQLite OpenAI 兼容接口 Whisper Edge-TTS。前端我做了一个极简 Web 页面用录音插件录音上传到后端转写Agent 处理完再返回音频。整体是一个“异步双工”模式虽然不如电话实时但足以验证教学闭环。3. 情景教学的核心Prompt 设计与场景编排3.1 让 Agent 记住“角色身份”的 System Prompt为什么很多口语 AI 一开口就像“机器人”问题出在 System Prompt 写成了“你是英语老师请帮助用户学习英语”这种 prompt 引导模型输出解释和说教。我要的是“你是一个咖啡店店员正在接待一位顾客”教学功能反而不应该出现在明面上。我的 System Prompt 分成四层角色定义、场景描述、教学策略、输出格式。角色定义写明身份、性格、说话风格场景描述写明地点、时间、正在发生的任务教学策略规定纠错时机的原则比如“不要打断用户说话”“连续错三次才给出慢速示范”输出格式则强制模型返回一个 JSON 对象包含用户回复、内部反馈、评分和下一步剧情。用结构化输出之后效果立刻不一样了。模型不再顾此失彼——它知道用户看的文本只应该是“店员说的话”而评分、提示、语法错误分析都放在隐藏字段里可以存库做学习报告。3.2 对话难度与纠错时机的控制教学 Agent 最容易犯的错是“过度纠正”。用户刚说一句 “I want a coffee”模型马上纠正 “Actually, more naturally we say ‘I’d like a coffee’”一句话说三次就被打断三次用户根本練不下去。我的做法是给模型一个分段纠错策略。第一轮只在用户出现影响理解的严重语法错误时才纠同一错误连续出现两次加入提示但不中断剧情连续错三次模型切换为“教练模式”用 slower speed 的句子重新示范并让用户复述。同时评分改为 0 到 100 分的连续值而不是 “Good” 这种模糊词分数变化能明显刺激用户重试。3.3 用状态机管住剧情流程纯靠 prompt 去指导模型“推进剧情”是不可靠的模型经常会提前把剧情讲完。所以我在外层加了一个简单的状态机每个场景拆成 4 到 6 个节点比如“开场问候—点单选择—特殊要求—支付结算—道别”。每次对话前后端把当前状态塞进 user message 里同时要求模型输出next_state由代码决定是否允许跳转。这个设计让我没有损失 LLM 的灵活性又增加了代码的确定性。比如在咖啡店场景用户如果突然说“有人晕倒了”模型可以跳转到“突发状况”节点而不是死板地按原流程走。状态机只做“允许跳转的目标约束”真正的剧情生成还是由模型完成。4. 从零到一的代码实现4.1 工程目录与依赖安装先给出一份可以直接跑的工程目录我用它做快速原型代码量不大但结构足够清晰。english_agent/ ├── main.py # FastAPI 入口 ├── agent_core.py # Agent 核心逻辑 ├── scene_state.py # 场景状态机 ├── voice.py # STT/TTS 封装 ├── prompts/ # prompt 模板 │ ├── cafe_system.txt │ └── airport_system.txt ├── static/ # 前端页面 │ └── index.html └── requirements.txt依赖安装很简单我用 pip 装了 fastapi、uvicorn、openai、whisper、edge-tts。注意 whisper 的依赖会隐式装 torch如果机器没有 CUDA就用 CPU 版先做功能验证推理慢一点无所谓。4.2 Agent 核心类的实现Agent 核心我不想写得太复杂它的职责只有三个保存会话历史、调用大模型、解析结构化输出。import json from openai import OpenAI class EnglishAgent: def __init__(self, api_key: str, base_url: str, model: str): self.client OpenAI(api_keyapi_key, base_urlbase_url) self.model model self.history [] def build_messages(self, system_prompt: str, user_text: str, current_state: str): messages [ {role: system, content: system_prompt}, {role: system, content: f当前剧情状态{current_state}}, ] messages.extend(self.history) messages.append({role: user, content: user_text}) # 强制模型输出 JSON messages.append({role: system, content: 请严格按 JSON 返回不要输出其他内容。}) return messages def chat(self, system_prompt: str, user_text: str, current_state: str) - dict: messages self.build_messages(system_prompt, user_text, current_state) resp self.client.chat.completions.create( modelself.model, messagesmessages, temperature0.7, response_format{type: json_object}, ) content resp.choices[0].message.content data json.loads(content) # 将本轮对话存入历史 self.history.append({role: user, content: user_text}) self.history.append({role: assistant, content: data.get(agent_response, )}) return data有一个重点要把response_format设为 JSON 对象模式否则模型可能在字符串里穿插解释性文字整个解析链路都会崩。如果用的是不支持 JSON Mode 的模型可以退而求其次在 prompt 里标明“不要输出 JSON 以外的内容”同时用正则兜底。4.3 场景状态机的实现状态机代码很短但它是整个 Agent 不跑偏的关键。每个场景预先定义好所有状态以及每个状态可以跳转的合法范围。class SceneStateMachine: def __init__(self, states: list, transitions: dict, initial: str): self.states states self.valid_transitions transitions self.state initial def can_transition_to(self, target: str) - bool: return target in self.valid_transitions.get(self.state, []) def transition(self, target: str): if target in self.states and self.can_transition_to(target): self.state target return True # 不合法跳转则停留在当前状态并给一个提示 return False从模型返回的 JSON 里会有一个next_state字段代码先校验它是否合法如果模型想跳得太多我会强制回到当前状态并追加一条系统消息“剧情不要推进太快先完成当前操作”。这样模型会收敛很多。比如咖啡店场景里“点单”状态只允许跳到“确认订单”或“询问 special requirement”不允许直接跳到“支付”。4.4 接入语音识别与语音合成语音链路我封装成两个函数。语音识别用 whisper 转写语音合成用 edge-tts 生成 MP3。import whisper import edge_tts model_whisper whisper.load_model(base) async def transcribe_audio(file_bytes: bytes) - str: # 先落盘成临时文件再转写简洁稳定 with open(tmp_audio.webm, wb) as f: f.write(file_bytes) result model_whisper.transcribe(tmp_audio.webm) return result[text] async def speak(text: str) - bytes: communicate edge_tts.Communicate(text, voiceen-US-JennyNeural, rate10%) audio b async for chunk in communicate.stream(): if chunk[type] audio: audio chunk[data] return audio这里有几个坑要提醒。第一Whisper 的base模型对带有口音的英文识别稍弱如果用户故意念错转写时可能直接“修正”成标准拼写导致评分失真。所以我后来加了一个参数让 whisper 开启languageen同时关闭自动修正。第二Edge-TTS 的声音是可选的en-US-JennyNeural更清晰但偏播音腔如果想要更生活化一点可以换en-US-AriaNeural或en-GB-SoniaNeural。4.5 用 FastAPI 把一切串起来我写了一个简单的 POST 接口前端录音后把音频文件传过来后端返回 JSON含 agent 回复文本、评分、音频 URL。from fastapi import FastAPI, File, UploadFile from fastapi.responses import FileResponse import scene_state, agent_core, voice, json app FastAPI() # 全局维护一个会话对象实际上应按 session_id 隔离 agent agent_core.EnglishAgent(api_keyxxx, base_urlhttps://api.openai.com/v1, modelgpt-4o-mini) state_machine scene_state.SceneStateMachine( states[greet, order, special, pay, bye], transitions{ greet: [order], order: [special, pay], special: [pay], pay: [bye], bye: [] }, initialgreet ) app.post(/chat) async def chat_audio(file: UploadFile File(...)): audio_bytes await file.read() user_text await voice.transcribe_audio(audio_bytes) system_prompt open(prompts/cafe_system.txt).read() result agent.chat(system_prompt, user_text, state_machine.state) ok state_machine.transition(result.get(next_state, )) if not ok: result[agent_response] (Lets finish the current step first.) tts_audio await voice.speak(result[agent_response]) with open(static/current_reply.mp3, wb) as f: f.write(tts_audio) return { user_text: user_text, agent_text: result[agent_response], feedback: result[feedback], score: result[score], next_state: state_machine.state, audio_url: /audio/current_reply.mp3 }接口不是生产级但作为原型足够跑通全流程。实际开发时你要把agent改成按 session_id 存储state_machine也一样否则两个用户同时用会串线。这一点我在测试时踩过两个浏览器标签页互相干扰数据全乱了。5. 常见问题与排查技巧实录5.1 Agent 回答太“老师范儿”氛围感全无我的第一版系统 prompt 里写了“You are a helpful English teacher”结果顾客永远不会说错店员反而开始讲课。后来我改了策略系统 prompt 只写“You are a busy café barista at a café in London”并且规定“在用户没有求助时你只是一个正常的店员在用户连续出错后你才切换成耐心教练”。另外要把“教学反馈”和“角色台词”分开。模型输出 JSON 里agent_response是用户听到/看到的店员说的话feedback才是隐藏的教学建议。这个分离让模型不用在“像真人”和“有帮助”之间纠结效果提升非常大。5.2 中文关键词混进英文对话评分失真用户很常见的问题是说着说着蹦出中文比如 “我要一杯 latte多少钱” Whisper 会把这句转写成中英混杂文本模型在判断时也容易迷惑。我的解法是在转写之后先做一个语言检测如果中文占比过高直接在反馈里提示“This sentence contains Chinese. Try saying it in English again.” 同时不对这一轮评分。连续三句都带中文就把提示词改成完整的中文解释先帮用户想好英文句式。这一步看着小但对用户体感提升巨大。它让用户明确知道 Agent 在“等他说英文”而不是在用中文混过去。5.3 多轮对话触发上下文溢出标准情景对话 10 轮以内基本没有问题但一旦实现“记忆用户常犯错误”上下文会越积越长。我的方案是实时消息只保留最近 8 轮更早的内容压缩成一个摘要字段。比如“用户之前三次把 ‘receive’ 拼成 ‘recieve’已经纠正两次仍需巩固”。每次请求前把摘要拼到 system prompt 里。这个方案比简单裁剪消息更符合教学 Agent 的需求因为记忆的核心不是逐字聊天记录而是“用户还没掌握的知识点”。后来我把摘要单独存在 SQLite 里每次对话结束让模型更新一次摘要长期记忆问题就解决了。5.4 并发请求如何扛住项目在群里小范围内测时来了 30 多个人同时用我的 CPU 直接被 Whisper 和 TTS 吃满接口大面积超时。后来我做了几件事。第一把 STT 和 TTS 改成异步后台任务用 FastAPI 的BackgroundTasks或者独立的队列接口先返回“处理中”。第二给语音转写加一个进程锁Whisper 同一个模型实例不能同时处理多个请求否则显存不够。第三在大模型调用层加了重试和超时。实测下来单人开发阶段这个趟程度已经够用真要上生产就得换成独立 GPU 或云端语音服务。千万不要一开始就想着分布式和高并发先把单机算力跑满、把异步化做好再考虑扩容。这个项目我栽过的跟头就是“过早优化”。6. 后续还能往哪些方向扩展6.1 引入知识库与真实语料现在 Agent 只能靠大模型的固有知识去模拟店员下一版我想把真实场景语料挂进来比如星巴克菜单的价格、机场安检的常见规定、伦敦地铁的线路运营规则让 Agent 的回答更准。实现方式很简单给 Agent 加一个检索工具当用户问到“latte 多少钱”时先去 FAQ 或菜单文档里检索再把检索结果放进 prompt 里。这样就不需要频繁改 system prompt业务人员也能独立维护语料了。我做这一版的时候用的是向量数据库但如果你只想验证效果直接用关键词检索 纯文本拼接就够。6.2 把“评分”升级成“多维度能力分析”当前评分只有 0 到 100 的单一分数对用户帮助有限。下一步我想把每个 JSON 反馈里的feedback字段细挖成几个维度语法准确度、流利度、词汇地道度、发音清晰度。发音清晰度可以结合 Whisper 的置信度分数虽然不是真正的音素评估但做相对参考已经够用。这个能力很容易做成个人学习报告。每次练习结束后前端展示雷达图“语法 82 分流利 70 分词汇 65 分”用户会明显感受到自己有哪些短板需要单独补。6.3 用 WebRTC 实现实时通话异步录音模式的好处是开发简单缺点是不够自然。用户说完一句话要等 1 到 2 秒没有真人对话的即时感。我现在的升级方向是 WebRTC 音频流利用 VAD 检测说话停顿后端做流式 STTTTS 流式返回音频块。难点在于双工通话时的打断用户正在听 AI 说话时突然插嘴系统需要立刻停止 TTS。这部分我还在试验如果你要做建议先参考成熟的语音 Agent 实现不要自己从零封装 WebRTC坑非常多。最后分享一点个人经验这个项目从立项到跑通第一版我大概用了 5 天真正花时间最多的地方反而不是语音也不是大模型调用而是“怎么让教学反馈自然融入对话”。如果你也想做类似的教育 Agent我建议先从文本对话开始把角色扮演、评分、纠错这三个闭环跑通再加语音。语音只是交互方式的升级核心的教学逻辑才是护城河。我在实际调试中还有一个很管用的技巧每次改 prompt 之前先准备 10 条固定的测试用户语句改完一轮就回归跑一遍看看“像个真人”“纠错时机”“状态推进”这三个指标有没有退化。Agent 开发的本质是反复实验用一套自己的测试集把模型行为“锁”住比每次凭感觉调参可靠得多。这个习惯我现在已经迁移到所有 Agent 项目里了非常值得坚持。
阅读完成 · 觉得有帮助?