ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

用大语言模型开发英语情景教学Agent:从设计到代码实现

用大语言模型开发英语情景教学Agent:从设计到代码实现

做这个项目的起因特别简单:我发现很多学员背单词能背到 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_key=api_key, base_url=base_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( model=self.model, messages=messages, temperature=0.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, voice="en-US-JennyNeural", rate="+10%") audio = b"" async for chunk in communicate.stream(): if chunk["type"] == "audio": audio += chunk["data"] return audio

这里有几个坑要提醒。第一,Whisper 的base模型对带有口音的英文识别稍弱,如果用户故意念错,转写时可能直接“修正”成标准拼写,导致评分失真。所以我后来加了一个参数:让 whisper 开启language="en",同时关闭自动修正。第二,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_key="xxx", base_url="https://api.openai.com/v1", model="gpt-4o-mini") state_machine = scene_state.SceneStateMachine( states=["greet", "order", "special", "pay", "bye"], transitions={ "greet": ["order"], "order": ["special", "pay"], "special": ["pay"], "pay": ["bye"], "bye": [] }, initial="greet" ) @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"] += " (Let's 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 检测说话停顿,后端做流式 STT,TTS 流式返回音频块。难点在于双工通话时的打断:用户正在听 AI 说话时突然插嘴,系统需要立刻停止 TTS。

这部分我还在试验,如果你要做,建议先参考成熟的语音 Agent 实现,不要自己从零封装 WebRTC,坑非常多。

最后分享一点个人经验

这个项目从立项到跑通第一版,我大概用了 5 天,真正花时间最多的地方反而不是语音,也不是大模型调用,而是“怎么让教学反馈自然融入对话”。如果你也想做类似的教育 Agent,我建议先从文本对话开始,把角色扮演、评分、纠错这三个闭环跑通,再加语音。语音只是交互方式的升级,核心的教学逻辑才是护城河。

我在实际调试中还有一个很管用的技巧:每次改 prompt 之前,先准备 10 条固定的测试用户语句,改完一轮就回归跑一遍,看看“像个真人”“纠错时机”“状态推进”这三个指标有没有退化。Agent 开发的本质是反复实验,用一套自己的测试集把模型行为“锁”住,比每次凭感觉调参可靠得多。这个习惯我现在已经迁移到所有 Agent 项目里了,非常值得坚持。

返回列表