🚀 [实战] 进化!从脚本到数字员工:打造 L5 级人机协同与全双工语音的 AI 面试官
摘要:今天是一个里程碑。我们将 JD_Agent 从一个简单的 RAG 问答工具,彻底重构为一个具备 L5 级别自主性 的多智能体系统。同时,我们攻克了 全双工语音交互 和 DeepSeek 风格思考过程 的工程难题,让 AI 不仅能“思考”,还能像真人一样“倾听”和“表达”。
🌟 架构全景图 (The Big Picture)
在 v2.0 版本中,我们不再简单的线性调用 LLM,而是构建了一个复杂的多智能体协作网络。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| graph TD User((🙍♂️ 用户)) <-->|Web Audio / SSE| Frontend[🖥️ Next.js 前端] Frontend <-->|HTTP / Stream| Backend[⚙️ FastAPI 后端]
subgraph "🧠 智能体大脑 (LangGraph)" Start(开始) --> Parser[🔍 职位解析员] Parser --> Researcher[🕵️ 商业情报员] Parser --> TechLead[💻 技术面试官] Researcher --> Context{信息汇总} TechLead --> Reviewer[⚖️ 质量检察员] Reviewer -->|评分 < 85| TechLead Reviewer -->|评分 >= 85| End(✅ 输出报告) Reviewer -.->|拿捏不准| Human[🛑 人工介入] Human --> TechLead end
subgraph "🔊 语音交互层" ASR[👂 Whisper ASR] TTS[🗣️ macOS Native TTS] end
Backend --> ASR Backend --> TTS Backend --> Start
|
核心突破一:L5 级多智能体协同 (LangGraph)
我们要解决的核心痛点是:AI 生成的内容往往不够深度,或者一本正经胡说八道。
解决方案是引入 Reviewer (质检员) 角色,形成质量闭环。
1. 定义“团队” (The Team)
我们不再是一个 Agent 打天下,而是通过 LangGraph 组建了一个虚拟团队:
- Tech Lead: 负责根据 JD 出题。
- Reviewer: 负责给题目打分。如果不合格,打回重写。
2. 代码实现 (Workflow)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
def qa_router(state: AgentState): if state["iteration_count"] > 3: return "approved" if state["quality_score"] >= 85: return "approved" else: return "rejected"
workflow = StateGraph(AgentState) workflow.add_node("tech_lead", tech_lead_node) workflow.add_node("reviewer", reviewer_node)
workflow.add_edge("tech_lead", "reviewer") workflow.add_conditional_edges( "reviewer", qa_router, { "approved": END, "rejected": "tech_lead" } )
|
核心突破二:DeepSeek 风格的“思考过程” UI
为了缓解 L5 架构推理时间长(可能达 1-2 分钟)带来的焦虑感,我们复刻了 DeepSeek R1 的交互体验:把 AI 的思考过程透明化。
1. 双流协议设计 (Dual Stream Protocol)
后端不再只返回文本,而是返回 JSON 事件流:
type: "thought" -> 思考步骤(如“正在检索财报…”)
type: "token" -> 最终回复内容
1 2 3 4 5
| data: {"type": "thought", "content": "正在分析 JD 技术栈..."} data: {"type": "thought", "content": "正在构思追问策略..."} data: {"type": "token", "content": "您好,"} data: {"type": "token", "content": "根据您的经历..."}
|
2. 前端可视化组件 (Thinking Block)
前端实现了一个可折叠的面板,实时渲染 thought 流。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| export default function ThinkingBlock({ thoughts, isFinished }) { return ( <div className="bg-gray-50 border rounded-xl p-4"> <div className="flex items-center gap-2 text-gray-500"> {isFinished ? <BrainCircuit /> : <Loader2 className="animate-spin"/>} <span>{isFinished ? "深度思考完成" : "DeepSeek 正在思考..."}</span> </div> {/* 动态渲染思考步骤 */} <ul className="mt-2 space-y-1 border-l-2 border-gray-200 pl-4"> {thoughts.map(step => ( <li className="text-xs text-gray-600 animate-in fade-in">{step}</li> ))} </ul> </div> ) }
|
核心突破三:全双工语音交互 (ASR + TTS)
我们实现了 录音 -> ASR -> LLM -> TTS 的完整闭环,让 AI 变身为真正的面试官。
1. 语音合成 (TTS) 的避坑之路
这是今天遇到的最大工程挑战。
- ❌ Edge-TTS: 在国内网络环境下频繁报
503 Service Unavailable,不稳定。
- ❌ Pyttsx3: 生成的 AIFF 格式在 Chrome 浏览器中无法播放 (
NotSupportedError)。
- ✅ macOS 原生
say 命令: 最终方案!
利用 Mac 系统底层的 say 命令生成 .m4a 文件,既利用了 Siri 的高质量语音,又做到了零网络延迟、零成本。
后端实现 (FastAPI):
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import subprocess
@router.post("/audio/tts") async def text_to_speech(text: str): """ Mac 专属黑科技:调用系统底层 TTS """ tmp_path = f"/tmp/{uuid.uuid4()}.m4a" subprocess.run(["say", "-o", tmp_path, text]) with open(tmp_path, "rb") as f: return Response(content=f.read(), media_type="audio/mp4")
|
2. 前端音频队列 (Audio Queue)
为了防止流式生成时语音重叠(AI 还在打字,上一句还没读完,下一句又来了),我们在前端实现了一个分句缓冲队列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| sequenceDiagram participant SSE as 后端流 participant Parser as 前端解析器 participant Queue as 音频队列 participant Player as 播放器
SSE->>Parser: "你好," SSE->>Parser: "我是" SSE->>Parser: "面试官。" (检测到标点) Parser->>Queue: 入队: "你好,我是面试官。" loop 队列监听 Queue->>Player: 取出第一句 Player-->>Player: 播放中... Player->>Queue: 播放结束 (onEnded) end
|
🛠️ 踩坑实录 (Troubleshooting)
🐛 Bug 1: React 严格模式下的“复读机”
- 现象:AI 回复出现 AABB 重复(如“好好…的的…”)。
- 原因:
Next.js 开发环境下 React.StrictMode 会执行两次 setState。而我们的代码中直接修改了对象属性 (msg.content += chunk)。
- 修复:严格遵循 Immutability 原则。
1 2 3 4 5 6 7
| setMessages(prev => { const newMsgs = [...prev]; newMsgs[lastIndex] = { ...lastMsg, content: lastMsg.content + chunk }; return newMsgs; });
|
🐛 Bug 2: Worker is not defined
- 现象:引入录音库
react-media-recorder 后页面崩溃。
- 原因:Next.js 服务端渲染 (SSR) 无法访问浏览器特有的 Worker API。
- 修复:使用
dynamic import 隔离加载。
1
| const ChatInput = dynamic(() => import("@/components/ChatInput"), { ssr: false });
|
🔮 总结
现在的 JD_Agent 已经不仅仅是一个代码 demo,它具备了:
- 大脑:懂反思、会纠错的多智能体团队。
- 嘴巴:零延迟的本地 TTS。
- 颜值:媲美 DeepSeek 的现代化 UI。
Next Step: 我们将尝试引入 WebRTC,将现在的“对讲机模式”升级为真正的“打断式实时通话”。
Keep Building, Keep Evolving. 🚀