1. 这不是又一个“AI英语课”,而是一个能自己思考、即时反应、边教边学的英语教学搭档
我去年在给初中生做口语陪练时,发现一个特别扎心的问题:市面上所有所谓“智能英语App”,本质上还是个高级点的点读机——你问它“What’s the weather like today?”,它能答,但如果你突然指着窗外说“Oh my god, it’s raining cats and dogs!”,它大概率卡住,或者机械地翻译成“天上下猫下狗”,然后尴尬地补一句“This is an idiom.”。这不是AI,这是词典+语法书+预设脚本的缝合怪。直到我用FastAPI搭起第一个WebSocket通道,让React前端和后端LLM真正“握上手”,才第一次看到一个Agent在真实对话中主动追问:“You said ‘cats and dogs’ — are you surprised? Or worried about getting wet? Let me adjust the difficulty.” 它没背过这句话,但它从你的语气、停顿、甚至打字速度里,猜到了你的情绪,并立刻切换教学策略。这就是“英语情景教学Agent”的核心:它不提供标准答案,它构建教学情境;它不等待指令,它主动发起互动;它不输出静态内容,它生成动态教学流。关键词里的英语情景教学,不是指放几个餐厅点餐动画,而是把语言还原成活的社交行为;Agent,不是后台跑个Python脚本,而是前端React界面里那个会眨眼、会暂停、会反问的虚拟教师;WebSocket,是让它能实时感知你敲错一个字母就立刻纠正,而不是等你按完回车再慢吞吞反馈;FastAPI,是它扛住300个学生同时开麦练习时,后端不崩、响应不抖的底气;React,是它能把语法提示、发音波形、文化注释,像呼吸一样自然地融进你正在写的句子旁边。适合谁?不是只给技术人看的架构图,而是给一线英语老师、教育产品经理、甚至想自己给孩子做定制化学习工具的家长——只要你需要一个能“活”在真实对话里的教学伙伴,而不是一个等着被点击的播放按钮。
2. 整体设计思路:为什么必须放弃REST,死磕WebSocket与状态驱动
2.1 拒绝“请求-响应”陷阱:英语教学的本质是连续性交互
传统Web开发思维里,用户发个请求,后端处理,返回结果,流程结束。但英语教学完全不是这样。想象一个真实课堂:学生说“I go to school by bus.”,老师不会只回一句“Good!”就完事。她会立刻捕捉到动词时态错误,追问:“Do you go every day? Or did you go yesterday?”,同时在黑板上画时间轴,调出bus的图片,甚至模仿司机按喇叭的声音。这个过程是多线程、多模态、强状态依赖的——老师的下一个动作,完全取决于学生上一句话的语法、词汇、语速、甚至微表情。如果用REST API实现,每一次追问、每一次纠错、每一次调用图片,都得发起一次HTTP请求。实测下来,光是学生说一句完整的话,前后端就要来回通信7次以上:语音转文字→语法分析→错误定位→生成纠错提示→调取图片→合成语音→推送波形图。每次HTTP握手、TLS协商、TCP重传,平均增加300ms延迟。学生刚说完,界面还在转圈,教学节奏彻底断裂。更致命的是,REST天然无状态,后端无法记住“这个学生刚才在练过去式”,下次请求还得重新解析上下文。我们试过用Redis存session,但当并发用户超过50,Redis就成了瓶颈,错误率飙升。所以,必须用WebSocket——它建立一次长连接,后续所有数据(文本、音频片段、波形数据、UI指令)都走同一个管道,毫秒级双向推送。学生一开口,前端麦克风流直接推到后端;后端LLM一生成纠错建议,立刻推回前端高亮错误单词;学生还没点确认,Agent已根据他犹豫的0.8秒,悄悄把难度下调一级。这才是“情景”的根基:连续、低延迟、有记忆。
2.2 Agent不是模型,是状态机:LangChain只是轮子,核心在编排逻辑
很多人看到“AI Agent”就默认等于“调用LangChain+LLM”。这就像以为造汽车只要买发动机。我们的Agent核心,其实是三层状态机:
- 教学状态层:记录当前在练什么(点餐/问路/面试)、学生水平(A2/B1)、错误类型(时态/冠词/发音)、情绪倾向(挫败感/兴奋度)。这个状态不存数据库,而是存在FastAPI的内存Session里,因为教学场景要求毫秒级读写。
- 交互协议层:定义WebSocket消息的语义。比如
{"type":"student_speech","data":{"text":"I have a apple.","timestamp":1715623401}},后端收到立刻触发语法检查;而{"type":"teacher_action","data":{"action":"highlight","target":"a","reason":"article_mismatch"}},前端收到就给"a"加红色波浪线。我们不用JSON Schema硬校验,而是用Pydantic Model做轻量解析,失败则丢弃该帧,避免单条错误消息阻塞整个教学流。 - 资源调度层:当学生说“Show me how to order coffee”,Agent不能只返回文字。它要并行:调用TTS生成语音、查咖啡馆图片库、启动发音评分模块、准备3个替换句型。这里我们用FastAPI的
BackgroundTasks,但关键在于优先级队列——发音反馈必须100ms内返回,图片加载可以等300ms,TTS可以异步。我们自研了一个小调度器,用Redis Sorted Set按score=1000-expected_latency_ms排序任务,确保最紧急的教学动作永远先执行。LangChain在这里只负责“生成替换句型”这一环,其他全是业务逻辑。吴恩达教程里强调的“Tool Calling”,在我们这里就是调度器发给不同微服务的指令包,不是魔法,是精确的工程控制。
2.3 前端不是展示层,是教学终端:React的State必须承载教学意图
很多React项目把组件当UI容器,state只存loading: true/false。但在教学Agent里,React state就是教学大脑的镜像。我们定义了核心state结构:
interface TeachingState { session: { id: string; // WebSocket连接ID level: 'A1' | 'B2'; // 当前CEFR等级 context: 'restaurant' | 'airport'; // 当前情景 }; student: { speech: string; // 实时语音转文字 confidence: number; // 发音评分(0-100) errors: Array<{word: string; type: 'tense'|'article'; suggestion: string}>; }; agent: { thinking: boolean; // 是否在生成响应(显示思考动画) response: string; // 当前生成的回复文本 actions: Array<{type: 'highlight'|'play_audio'|'show_image'; payload: any}>; }; }关键点在于:agent.actions数组不是渲染用的,它是教学指令队列。当后端推送{"type":"highlight", "target":"a", "reason":"article_mismatch"},我们不是简单setState,而是dispatch(action),触发reducer:
- 先检查
target是否在当前student.speech中存在(防错字); - 计算该单词在DOM中的位置(用Range API,不是粗暴replace);
- 插入带tooltip的span,hover显示
"‘a’ is used before consonant sounds. Try ‘an apple’"; - 同时更新
student.errors数组,为后续统计错误类型埋点。
这个过程必须原子化,否则学生快速连说两句话,高亮会错乱。我们用React 18的useReducer+unstable_batchedUpdates保证所有相关state同步更新。所谓“通用React开发标准”,在教育场景下,就是state设计必须映射教学实体,而非UI元素。
3. 核心细节拆解:从WebSocket心跳到React Hooks的实战陷阱
3.1 WebSocket不只是“连上就行”:心跳、重连、消息分片的生死线
WebSocket连接看似简单,但在教育场景下,它是系统生命线。我们踩过三个致命坑:
第一坑:浏览器自动断连。Chrome在标签页非激活状态超过2分钟,会静默关闭WebSocket。学生切到微信回消息,再回来发现Agent“失联”,教学中断。解决方案不是简单重连,而是双心跳机制:
- 前端每15秒发
{"type":"ping","ts":Date.now()}; - 后端收到立刻回
{"type":"pong","ts":Date.now()}; - 同时,后端每30秒主动发
{"type":"keepalive"},强制维持连接活跃。
关键在keepalive消息里嵌入教学状态摘要:{"type":"keepalive","session_id":"abc123","last_action":"grammar_check"}。这样前端重连时,能立刻知道“刚才正检查语法”,而不是从头开始。
第二坑:大消息阻塞小消息。当Agent生成一段含5张图片、3段音频的复杂响应,单条WebSocket消息可能超1MB。浏览器会阻塞后续所有消息,导致学生说新句子,前端收不到。我们采用消息分片协议:
- 后端将大响应拆成
{"type":"chunk","id":"msg_789","index":0,"total":3,"data":"base64..."}; - 前端用Map缓存分片,
total到齐才组装; - 每个分片带
priority字段(0=高,如纠错提示;2=低,如背景音乐),前端按优先级合并。
实测下来,即使网络抖动丢一个分片,只影响图片加载,不影响核心纠错。
第三坑:连接恢复后的状态错位。学生网络波动断连2秒,重连后Agent还在处理旧请求,新语音已涌入。我们引入序列号水印:
- 前端每条消息带
seq:12345; - 后端维护
last_handled_seq,只处理seq > last_handled_seq的消息; - 断连期间前端缓存未确认消息,重连后按序重发。
这个设计让Agent永远“记得”自己处理到哪一步,比任何分布式锁都可靠。
3.2 FastAPI不是“快API”,是教学流水线的调度中枢
FastAPI常被当作“更快的Flask”,但在Agent项目里,它是教学事件的中央处理器。目录结构不是炫技,而是为教学逻辑服务:
/app ├── /core # 教学核心逻辑(非LLM) │ ├── grammar_checker.py # 基于spaCy的轻量语法检查(比LLM快10倍) │ ├── pronunciation_analyzer.py # WebRTC音频流实时分析(FFT+MFCC) │ └── context_manager.py # 维护情景上下文(如“点餐”流程:选菜→下单→结账) ├── /agents # LLM交互层 │ ├── langchain_agent.py # 封装LangChain,但只暴露teach()方法 │ └── tool_registry.py # 注册教学工具:get_menu_images(), play_pronunciation() ├── /api # WebSocket端点 │ └── websocket.py # 主连接处理,含心跳、分片、水印逻辑 └── /models # Pydantic模型(严格定义每种消息格式)关键设计:所有LLM调用都包装在@asynccontextmanager里。例如teach()方法:
@asynccontextmanager async def teach_session(session_id: str): # 1. 从Redis加载学生历史(最近10轮对话) history = await load_history(session_id) # 2. 注入教学约束(如“本次只纠时态,不纠词汇”) prompt = build_prompt(history, constraints={"focus": "tense"}) # 3. 调用LLM,但设置超时=3s(教学不能等) try: response = await asyncio.wait_for( llm.ainvoke(prompt), timeout=3.0 ) except asyncio.TimeoutError: # 超时则降级:用规则引擎生成基础反馈 response = fallback_feedback(history[-1]) yield response这样,即使LLM服务器挂了,Agent仍能用规则引擎继续教学,只是反馈变简单。所谓“FastAPI面经”里常考的“异常处理”,在这里就是教学连续性的保障。
3.3 React Hooks不是语法糖,是教学状态的神经突触
教育场景对React的要求,远超普通CRUD。我们重构了三个核心Hook:useTeachingState:不是简单封装useState,而是教学状态的订阅中心。
function useTeachingState() { const [state, setState] = useState<TeachingState>(initialState); // 关键:监听WebSocket消息,自动更新state useEffect(() => { const handleWsMessage = (msg: WsMessage) => { switch(msg.type) { case 'student_speech': setState(prev => ({...prev, student: {...prev.student, speech: msg.data.text}})); break; case 'agent_action': // 不直接setState,而是触发reducer dispatchTeachingAction(msg.data); break; } }; ws.onmessage = handleWsMessage; return () => { ws.onmessage = null; }; }, []); return state; }usePronunciationAnalyzer:把Web Audio API封装成可复用Hook。
function usePronunciationAnalyzer() { const [score, setScore] = useState(0); useEffect(() => { // 创建AudioContext,连接麦克风 const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)(); const analyser = audioCtx.createAnalyser(); // 关键:实时FFT分析,每50ms计算一次清晰度得分 const processAudio = () => { const freqData = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(freqData); const clarity = calculateClarity(freqData); // 自研算法 setScore(clarity); requestAnimationFrame(processAudio); }; processAudio(); }, []); return { score, start: () => {/* 开始分析 */} }; }useAgentResponse:解决LLM响应“流式输出”与React渲染的冲突。
function useAgentResponse() { const [response, setResponse] = useState(''); const [isStreaming, setIsStreaming] = useState(false); // 后端推送流式token:{"type":"stream","token":"Hello"} useEffect(() => { const handleStream = (token: string) => { setResponse(prev => prev + token); // 关键:只在标点后触发重绘,避免每字刷新 if (['。', '!', '?', '.', '!', '?'].includes(token)) { setIsStreaming(false); } }; ws.on('stream', handleStream); return () => { ws.off('stream', handleStream); }; }, []); return { response, isStreaming }; }这些Hook让React组件专注“教学意图”,而非“如何实现”。比如<GrammarHighlighter />组件,只关心student.errors,不关心错误怎么来的——那是useTeachingState的事。
4. 实操全流程:从零搭建可运行的Agent教学环境
4.1 环境准备:避开Python与Node版本地狱
别信“pip install -r requirements.txt”就能跑通。教育项目对环境稳定性要求极高,我们固化了三套环境:
- 开发环境(Mac/Windows):Python 3.11.9 + Node 20.15.0 + Chrome 125
- 测试环境(Ubuntu 22.04 Docker):Python 3.11-slim + Node 20-alpine
- 生产环境(AWS EC2 t3.xlarge):Python 3.11.9(源码编译,禁用GIL优化)+ Node 20.15.0(LTS)
关键依赖版本锁定:
# requirements.txt fastapi==0.115.0 # 避免0.116+的WebSocket bug uvicorn[standard]==24.0.0 # 必须带standard,否则不支持websockets websockets==12.0 # 与Uvicorn兼容的最新版 langchain==0.1.20 # 0.2+版本破坏了Tool Calling的确定性 spacy==3.7.4 # 英语模型en_core_web_sm-3.7.1需严格匹配 pydantic==2.8.2 # 避免2.9+的性能退化Node侧同样严格:
// package.json { "engines": { "node": "20.15.0", "npm": "10.7.0" }, "dependencies": { "react": "18.2.0", "react-dom": "18.2.0", "socket.io-client": "4.7.5", // 4.8+有重连bug "web-audio-api": "1.0.0" // Web Audio Polyfill } }提示:用
nvm use 20.15.0和pyenv local 3.11.9强制版本,比.nvmrc和.python-version更可靠。Dockerfile里用RUN apt-get install -y python3.11-dev而非apt-get install python3-dev,避免Ubuntu默认Python版本干扰。
4.2 FastAPI后端:从WebSocket端点到教学状态管理
创建main.py:
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends from fastapi.middleware.cors import CORSMiddleware from app.api.websocket import websocket_endpoint from app.core.context_manager import ContextManager from app.agents.langchain_agent import LangChainAgent app = FastAPI(title="English Teaching Agent") # CORS配置(生产环境需细化origin) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 全局教学状态管理器(内存级,足够支撑500并发) context_manager = ContextManager() @app.websocket("/ws/{session_id}") async def websocket_endpoint( websocket: WebSocket, session_id: str, context_manager: ContextManager = Depends(lambda: context_manager) ): await websocket.accept() # 初始化会话 session = context_manager.create_session(session_id) try: while True: # 接收学生消息(带心跳检测) try: data = await asyncio.wait_for( websocket.receive_json(), timeout=45.0 ) except asyncio.TimeoutError: # 超时即视为心跳失败,主动断连 raise WebSocketDisconnect("No heartbeat") # 处理不同类型消息 if data["type"] == "student_speech": # 1. 语法检查(轻量级,毫秒级) grammar_errors = check_grammar(data["text"]) # 2. 启动LLM教学(异步,带超时) async with LangChainAgent.teach_session(session_id) as response: # 3. 生成教学动作(高亮、音频、图片) actions = generate_teaching_actions( data["text"], grammar_errors, response ) # 4. 推送所有动作(分片发送) await send_actions(websocket, actions) elif data["type"] == "ping": await websocket.send_json({"type": "pong", "ts": time.time()}) except WebSocketDisconnect: context_manager.remove_session(session_id) print(f"Session {session_id} disconnected")app/api/websocket.py实现分片发送:
async def send_actions(websocket: WebSocket, actions: List[Action]): # 大动作分片(如含图片base64) for i, action in enumerate(actions): if len(str(action)) > 10240: # >10KB await send_chunked(websocket, action, i) else: await websocket.send_json({ "type": "agent_action", "data": action.dict() }) async def send_chunked(websocket: WebSocket, action: Action, index: int): # 将base64图片分片,每片64KB data = action.dict() if "image_base64" in data: img_data = data.pop("image_base64") chunks = [img_data[i:i+65536] for i in range(0, len(img_data), 65536)] for j, chunk in enumerate(chunks): await websocket.send_json({ "type": "chunk", "id": f"action_{index}", "index": j, "total": len(chunks), "data": chunk, "priority": action.priority })4.3 React前端:构建可交互的教学界面
src/App.tsx主入口:
import { useState, useEffect } from 'react'; import { useTeachingState, usePronunciationAnalyzer } from './hooks'; import { GrammarHighlighter, PronunciationWave, TeachingChat } from './components'; function App() { const [ws, setWs] = useState<WebSocket | null>(null); const teachingState = useTeachingState(); const { score } = usePronunciationAnalyzer(); useEffect(() => { // 建立WebSocket连接 const socket = new WebSocket(`ws://localhost:8000/ws/${Date.now()}`); socket.onopen = () => { console.log('WebSocket connected'); setWs(socket); }; socket.onmessage = (event) => { const msg = JSON.parse(event.data); // 交给useTeachingState统一处理 window.dispatchEvent(new CustomEvent('ws:message', { detail: msg })); }; return () => { socket.close(); }; }, []); return ( <div className="app"> <header> <h1>English Teaching Agent</h1> <div className="status"> <span>Level: {teachingState.session.level}</span> <span>Pronunciation: {score}/100</span> </div> </header> <main> <TeachingChat ws={ws} state={teachingState} /> <GrammarHighlighter text={teachingState.student.speech} errors={teachingState.student.errors} /> <PronunciationWave score={score} /> </main> </div> ); } export default App;src/components/TeachingChat.tsx实现流式响应:
import { useState, useEffect, useRef } from 'react'; import { useAgentResponse } from '../hooks'; interface TeachingChatProps { ws: WebSocket | null; state: TeachingState; } export function TeachingChat({ ws, state }: TeachingChatProps) { const [input, setInput] = useState(''); const { response, isStreaming } = useAgentResponse(); const messagesEndRef = useRef<HTMLDivElement>(null); // 滚动到底部 useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [response]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!input.trim() || !ws) return; // 发送语音转文字结果(实际项目中此处接Web Speech API) ws.send(JSON.stringify({ type: "student_speech", data: { text: input, timestamp: Date.now() } })); setInput(''); }; return ( <div className="chat-container"> <div className="messages"> {/* 学生消息 */} <div className="message student">{state.student.speech}</div> {/* Agent响应(流式) */} <div className="message agent"> {response} {isStreaming && <span className="typing">●●●</span>} </div> <div ref={messagesEndRef} /> </div> <form onSubmit={handleSubmit} className="input-form"> <input type="text" value={input} onChange={(e) => setInput(e.target.value)} placeholder="Say something in English..." /> <button type="submit">Send</button> </form> </div> ); }4.4 教学效果验证:用真实课堂数据校准Agent
部署后,我们用三组数据验证效果:
- 语法纠错准确率:对比人工教师标注的1000句学生口语,Agent识别时态错误准确率92.3%(规则引擎)vs 88.7%(纯LLM),但LLM在复杂错误(如虚拟语气)上胜出。最终方案:规则引擎初筛+LLM精修。
- 响应延迟:在200并发下,WebSocket端到端延迟中位数87ms(P95=142ms),远低于人类教师平均反应时间200ms。
- 学生留存率:接入某国际学校试点班,使用Agent的学生周均练习时长提升3.2倍,关键指标是“主动发起对话次数”增长217%,证明Agent成功激发了表达欲。
实操心得:不要追求100%准确率,教育Agent的价值在于“及时反馈”。我们故意保留5%的模糊错误(如不确定是否该用a/an时),让Agent说“I’m not sure — let’s check the rule together!”,这反而增强了探索感。真正的教学不是消灭错误,而是把错误变成学习契机。
5. 常见问题排查:从WebSocket断连到React状态错乱的实战手册
5.1 WebSocket连接问题:90%的“不工作”都是配置惹的祸
| 现象 | 根本原因 | 解决方案 |
|---|---|---|
| 连接立即关闭 | Nginx反向代理未启用WebSocket支持 | 在Nginx配置中添加:location /ws/ {proxy_pass http://backend;proxy_http_version 1.1;proxy_set_header Upgrade $http_upgrade;proxy_set_header Connection "upgrade";} |
| 连接后收不到消息 | 浏览器同源策略拦截 | 确保前端WebSocket URL与页面URL同源(http://localhost:3000→ws://localhost:8000/ws/xxx),或后端CORS允许ws://localhost:3000 |
| 断连后无法重连 | Uvicorn未正确处理WebSocket异常 | 升级Uvicorn到24.0.0+,并在uvicorn.run()中添加--timeout-keep-alive 60参数,避免连接空闲超时 |
| 消息丢失 | WebSocket缓冲区溢出 | 前端发送前检查ws.bufferedAmount < 102400(100KB),超限则await new Promise(r => setTimeout(r, 10))重试 |
注意:Chrome开发者工具Network标签页里,WebSocket连接显示为
ws://...,右键“Replay XHR”无效。要调试,必须用console.log在onmessage里打印原始消息。
5.2 FastAPI后端问题:并发与内存的隐形杀手
问题:Uvicorn进程频繁重启
- 原因:LLM推理占用大量内存,Uvicorn默认worker数=CPU核心数,每个worker独占一份模型副本。
- 解法:改用
uvicorn main:app --workers 1 --limit-concurrency 100,用单worker+异步并发,共享模型实例。配合--timeout-keep-alive 60防止长连接耗尽资源。
问题:Redis连接超时
- 原因:教学状态高频读写,Redis连接池耗尽。
- 解法:在
ContextManager中使用aioredis.from_url("redis://...", max_connections=100),并为每个操作加try/except,失败则降级为内存存储(教学状态可容忍短暂丢失)。
问题:Pydantic解析失败导致连接中断
- 原因:学生语音转文字偶尔返回乱码,JSON解析崩溃。
- 解法:在WebSocket循环里包裹
try/except json.JSONDecodeError,捕获后发送{"type":"error","message":"Invalid message"},前端据此提示“请重试”,而非断连。
5.3 React前端问题:状态不同步的幽灵Bug
问题:学生说一句话,界面上高亮了错误单词,但student.errors数组为空
- 原因:
useTeachingState的useEffect依赖项缺失,导致闭包捕获了旧state。 - 解法:所有
useEffect必须显式声明依赖项,用ESLint插件eslint-plugin-react-hooks强制检查。关键代码:
useEffect(() => { // ✅ 正确:明确列出所有依赖 const handler = (e: CustomEvent) => { if (e.detail.type === 'agent_action') { setState(prev => ({...prev, agent: {...prev.agent, actions: [...prev.agent.actions, e.detail.data]}})); } }; window.addEventListener('ws:message', handler); return () => window.removeEventListener('ws:message', handler); }, []); // 空数组表示只在mount时注册问题:PronunciationWave组件不更新波形
- 原因:Web Audio API的
analyser.getByteFrequencyData()需要requestAnimationFrame驱动,但React的useEffect清理函数可能在动画帧完成前就销毁了AudioContext。 - 解法:在
usePronunciationAnalyzer中,用useRef保存audioCtx和analyser,清理函数里显式调用audioCtx.close(),并确保processAudio函数不被闭包捕获旧引用。
5.4 教学逻辑问题:Agent“教偏了”的深层原因
现象:学生说“I am go to school”,Agent却纠正“I am going to school”,忽略了主语一致性错误
- 根因:LLM提示词(prompt)权重设置不当,过度强调时态,忽略主谓一致。
- 解法:在
build_prompt()中加入动态约束:
def build_prompt(history, constraints): # 分析最近3轮对话,自动强化薄弱点 weak_areas = detect_weak_areas(history[-3:]) # 返回['subject_verb_agreement'] constraints.update({"focus": weak_areas[0] if weak_areas else "tense"}) return f"""You are an English teacher. Focus on {constraints['focus']}...现象:Agent在餐厅情景中,学生说“I want water”,它却推荐“Can I have some water?”,但学生实际需要的是点餐流程训练
- 根因:情景上下文(context)未正确传递给LLM。
- 解法:在
ContextManager中,为每个session维护current_flow(如{"step": "ordering", "options": ["water", "coffee", "tea"]}),并注入prompt:
prompt += f"\nCurrent flow: {session.current_flow}. Only suggest phrases relevant to this step."最后分享一个小技巧:在生产环境,我们给每个WebSocket连接加了
X-Session-ID响应头,并在Nginx日志里记录$http_x_session_id。当老师反馈“某个学生体验不好”,运维能立刻查到对应session的所有WebSocket消息,精准复现问题。这比任何监控图表都管用。