1. 项目概述:一个可审计、可追踪、可交互的智能体落地实践
“AI 全栈学习之旅 - Week 11:从 CLI 到浏览器:用 FastAPI、SSE 和 Vue 3 做一个可审核的 ReAct Agent”——这个标题不是教学大纲里的抽象概念,而是我在真实项目中反复打磨出的一条技术路径。它解决的是当前大模型应用落地中最棘手的三个现实问题:执行过程不可见、决策链条不可追溯、用户交互不自然。你可能已经用过 LangChain 或 LlamaIndex 搭过一个能回答问题的 Agent,但当业务方问“它为什么选这一步?中间调用了哪个工具?哪次 API 调用失败了?”,你答不上来;或者用户在网页里等了8秒,页面突然报错“Stream disconnected before completion: idle timeout waiting for sse”,你只能重启服务。这就是本项目要直面的战场。
核心关键词 FastAPI、SSE、Vue 3、ReAct Agent、CLI 并非随意堆砌。FastAPI 是后端骨架,它不是为了“快”而选,而是因为它原生支持异步流式响应、自动 OpenAPI 文档、极简依赖管理,且对 Pydantic 模型的强约束让 Agent 的输入/输出协议天然可验证;SSE(Server-Sent Events)不是替代 WebSocket 的妥协方案,而是针对“单向、低频、高可靠性推送”的最优解——Agent 的思考步骤(Thought)、工具调用(Action)、观察结果(Observation)天然就是一条单向时间线,SSE 的文本流格式(event: xxx\ndata: {...}\n\n)比 WebSocket 的二进制帧更易调试、更易被浏览器 DevTools 捕获、更少受代理/CDN 断连干扰;Vue 3 的 Composition API 和响应式系统,让前端能精准映射 Agent 的每一步状态变更,而不是靠 setInterval 轮询或“loading...”模糊提示糊弄用户;ReAct Agent 不是炫技,它是把“推理-行动-观察”循环显式结构化,让每一步都带 timestamp、step_id、tool_name、input_params、raw_output,为后续审计日志、效果回溯、错误归因提供原子级数据支撑;CLI 则是整个系统的“最小可信入口”——它绕过所有 UI 层,直接暴露 Agent 的核心能力,既是开发时快速验证逻辑的探针,也是上线后运维人员排查问题的命令行终端。我试过用纯 Web UI 启动 Agent,结果发现用户反馈“卡住了”,但日志里没有任何报错,最后定位到是前端 SSE 连接在 Chrome 里被默认 idle timeout(30秒)断开,而 FastAPI 默认的超时设置是 60 秒,两者不匹配导致流中断。这种细节,只有真正跑通 CLI → API → Browser 全链路的人才会踩到。
适合谁来参考?如果你正在用 LangChain 写 Agent,但团队 QA 总说“流程黑盒,没法测”;如果你的 FastAPI 接口返回 JSON 很快,但推送到前端的思考流总断;如果你的 Vue 3 项目里 Volar 插件报错“Vue language features (Volar) is new recommended”,却不知道它和 TypeScript 类型推导、SSE 数据流绑定的关系;或者你刚装完 codex cli 却遇到 “unable to locate the codex cli binary or required runtime components”,其实那只是环境变量没配对——这篇就是为你写的。它不讲“什么是 SSE 协议”,而是告诉你怎么在 FastAPI 里写一个永不超时的流式 endpoint;不教“Vue 3 怎么用 ref”,而是展示如何用 reactive() 精确追踪 Agent 的 step-by-step 状态树;不罗列 “ReAct Agent 的 5 个组件”,而是拆解一个真实任务:让用户输入“查上海今天天气”,Agent 自动调用天气 API,再把结果总结成一句话,全程每一步都可审计、可重放、可截图存档。
2. 整体架构设计与技术选型逻辑
2.1 为什么是 ReAct,而不是 Chain-of-Thought 或 Plan-and-Execute?
ReAct(Reasoning + Acting)的核心价值在于它的可审计性设计。CoT(Chain-of-Thought)只输出推理链,比如“上海今天天气热,所以需要空调”,但它不告诉你这个结论基于哪个 API 返回的数据;Plan-and-Execute 先生成完整计划再执行,一旦某步失败,整个计划就崩盘,无法局部修复。而 ReAct 是“走一步,看一眼,再决定下一步”,每个循环包含明确的三元组:Thought(为什么做这一步)、Action(调用什么工具)、Observation(工具返回什么)。这三者天然构成审计日志的最小单元。我在实际项目中给金融风控场景做 Agent,监管要求“所有决策必须有可追溯的数据源”,ReAct 的Action字段直接记录调用的内部风控 API 地址、请求参数哈希值,Observation存储原始响应 body 的 SHA256,审计员只需比对哈希就能确认数据未被篡改。这不是理论优势,是合规刚需。
提示:不要把 ReAct 当成一种 Prompt 技巧。它必须在代码层强制结构化。我见过太多项目把 ReAct 写在 system prompt 里,结果 LLM 输出格式混乱,前端解析失败。正确做法是定义 Pydantic 模型:
class ReActStep(BaseModel): step_id: str = Field(default_factory=lambda: str(uuid4())) timestamp: datetime = Field(default_factory=datetime.now) thought: str action: str # 工具名,如 "weather_api" action_input: dict observation: str is_final: bool = False所有 Agent 步骤必须通过此模型序列化,否则“可审核”就是空谈。
2.2 为什么选 SSE 而非 WebSocket 或轮询?
WebSocket 看似强大,但它引入了连接管理复杂度:客户端需处理重连、心跳、消息序号;服务端要维护连接池、处理并发写入冲突;更重要的是,它不兼容 HTTP 缓存和 CDN——当你想用 Cloudflare 缓存静态资源时,WebSocket 流会直接被拦截。轮询(Polling)则完全违背实时性原则,每 2 秒发一次 GET 请求,不仅浪费带宽,还会因网络抖动导致步骤显示延迟甚至乱序。SSE 完美规避这些:它基于 HTTP/1.1,天然支持代理、CDN、HTTPS;浏览器自动处理重连(retry: 3000);服务端只需按规范输出event: step\ndata: {...}\n\n,无需管理连接状态;且 Chrome DevTools 的 Network 标签页能直接看到每条 event 的时间戳和 payload,调试效率极高。
注意:SSE 的 “idle timeout waiting for sse” 错误,90% 源于 FastAPI 的超时配置与反向代理(如 Nginx)的 timeout 设置不一致。FastAPI 默认 uvicorn 的
--timeout-keep-alive是 5 秒,而 Nginx 的proxy_read_timeout默认 60 秒。当 Agent 思考耗时超过 5 秒,uvicorn 主动关闭连接,但 Nginx 还在等,最终报错。解决方案是统一设为 300 秒,并在 FastAPI endpoint 中显式设置response.timeout = 300。
2.3 为什么用 FastAPI + Vue 3 分离,而非 Flask + Jinja 或 Next.js?
Flask + Jinja 是服务端渲染,Agent 的每一步思考都要刷新整个页面,用户体验割裂;Next.js 虽然支持 SSR,但其 App Router 的 streaming 机制与 ReAct 的 step-by-step 推送不匹配,容易出现“先显示 final answer,再补上中间步骤”的倒置现象。FastAPI + Vue 3 的组合,本质是“API First”架构:FastAPI 只负责业务逻辑和流式数据生成,Vue 3 只负责状态映射和 UI 渲染,二者通过清晰的 contract(OpenAPI Spec)解耦。这带来三个硬性好处:第一,前端可独立部署在 CDN 上,后端 API 可部署在私有云,安全边界清晰;第二,当需要接入飞书机器人或企业微信时,只需复用同一套 FastAPI endpoint,无需重写逻辑;第三,Vue 3 的<script setup>语法配合reactive(),能让 Agent 的状态树(steps: ReActStep[])与 DOM 完全同步,新增一个 step,列表自动追加,无需手动this.$forceUpdate()。
2.4 CLI 作为“信任锚点”的工程意义
CLI 不是锦上添花的功能,而是整个系统的“信任锚点”。当 Web UI 出现异常,运维第一反应不是看浏览器控制台,而是运行python cli.py --task "上海天气"。如果 CLI 正常输出完整的 step 流,说明 Agent 核心逻辑、工具集成、LLM 调用全部正常,问题一定出在前端 SSE 连接或 FastAPI 的流式响应配置上。反之,如果 CLI 也失败,则问题在数据层。我在某次上线前压测中,发现 Web UI 在 100 并发下大量报错,但 CLI 在同样负载下稳定运行。最终定位到是 Vue 3 的onBeforeUnmount钩子未正确清理 SSE EventSource,导致内存泄漏,旧连接未关闭就新建连接,耗尽浏览器最大连接数(Chrome 为 6 个)。没有 CLI,这个问题会被误判为后端性能瓶颈。
3. 核心模块实现与关键细节解析
3.1 FastAPI 后端:构建可审计的流式 Agent Endpoint
FastAPI 的核心在于将 ReAct Agent 的执行过程转化为符合 SSE 规范的流式响应。关键不是“怎么返回数据”,而是“怎么保证每一步都可靠送达、不丢失、不错序”。
首先,定义清晰的请求/响应模型。/v1/agent/run的 POST body 必须包含task: str和可选的session_id: str(用于审计日志关联):
class AgentRunRequest(BaseModel): task: str = Field(..., min_length=1, max_length=500) session_id: Optional[str] = None class AgentRunResponse(BaseModel): session_id: str status: Literal["started", "completed", "failed"]Endpoint 实现需注意三点:异步流控、超时防护、审计日志注入。
@app.post("/v1/agent/run", response_model=AgentRunResponse) async def run_agent( request: AgentRunRequest, background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db) ): # 1. 生成唯一 session_id(若未提供) session_id = request.session_id or str(uuid4()) # 2. 初始化 Agent(此处用 LangChain 的 ReActAgent,但需包装) agent = AuditableReActAgent( llm=ChatOpenAI(model="gpt-4-turbo"), tools=[WeatherTool(), SearchTool()], # 关键:注入审计上下文 audit_context={"session_id": session_id, "user_ip": request.client.host} ) # 3. 创建 SSE 响应流 async def event_stream(): try: # 记录审计日志:任务启动 await log_audit_event(db, session_id, "task_started", {"task": request.task}) # 执行 Agent,yield 每一步 async for step in agent.astream(request.task): # 强制序列化为 SSE 格式 yield f"event: step\ndata: {json.dumps(step.dict(), default=str)}\n\n" # 每步后主动 flush,避免缓冲区积压 await asyncio.sleep(0.01) # 防止 CPU 占用过高 # 最终状态 yield f"event: completed\ndata: {json.dumps({'session_id': session_id}, default=str)}\n\n" except Exception as e: # 记录错误审计日志 await log_audit_event(db, session_id, "task_failed", {"error": str(e)}) yield f"event: error\ndata: {json.dumps({'error': str(e)}, default=str)}\n\n" # 4. 设置超时(覆盖 uvicorn 默认) response = StreamingResponse( event_stream(), media_type="text/event-stream", headers={ "Cache-Control": "no-cache", "Connection": "keep-alive", } ) response.timeout = 300 # 显式设置 5 分钟超时 return responseAuditableReActAgent是关键封装类。它继承自 LangChain 的AgentExecutor,但重写了astream方法,确保每一步都经过审计模型校验:
class AuditableReActAgent(AgentExecutor): def __init__(self, *args, **kwargs): self.audit_context = kwargs.pop("audit_context", {}) super().__init__(*args, **kwargs) async def astream(self, input: str) -> AsyncIterator[ReActStep]: # 使用 LangChain 的 RunnableWithFallbacks 处理工具调用失败 for step in await self._execute_with_fallback(input): # 注入审计上下文 step_dict = step.dict() step_dict.update(self.audit_context) # 保存到数据库(异步) await save_step_to_db(step_dict) yield ReActStep(**step_dict)实操心得:
await asyncio.sleep(0.01)看似微小,却是防止 uvicorn worker 被长连接阻塞的关键。实测中,去掉这行,10 个并发 SSE 连接就会导致 uvicorn 主进程 CPU 100%,新请求排队。这是 FastAPI 官方文档未强调的实战技巧。
3.2 Vue 3 前端:精准映射 ReAct 状态流
Vue 3 的核心挑战不是“怎么接收 SSE”,而是“怎么让每一步思考在 UI 上精准、无闪烁地呈现”。常见错误是用v-for直接遍历steps数组,但 SSE 的data是字符串,需手动JSON.parse(),且EventSource的onmessage回调不在 Vue 的响应式系统内,直接push()会导致视图不更新。
正确做法是使用reactive()创建状态对象,并用onBeforeUnmount清理连接:
<script setup lang="ts"> import { reactive, onBeforeUnmount, ref } from 'vue' interface ReActStep { step_id: string timestamp: string thought: string action: string action_input: Record<string, any> observation: string is_final: boolean } const state = reactive({ steps: [] as ReActStep[], session_id: '', status: 'idle' as 'idle' | 'running' | 'completed' | 'error', error: '' }) const eventSource = ref<EventSource | null>(null) const startAgent = async (task: string) => { // 1. 清理旧连接 if (eventSource.value) { eventSource.value.close() } // 2. 创建新连接 eventSource.value = new EventSource(`/api/v1/agent/run?task=${encodeURIComponent(task)}`) // 3. 监听事件 eventSource.value.addEventListener('step', (e: MessageEvent) => { try { const step = JSON.parse(e.data) as ReActStep state.steps.push(step) // reactive 数组,自动触发更新 state.session_id = step.session_id state.status = 'running' } catch (err) { console.error('Parse step failed:', err) } }) eventSource.value.addEventListener('completed', (e: MessageEvent) => { state.status = 'completed' }) eventSource.value.addEventListener('error', (e: MessageEvent) => { state.status = 'error' state.error = JSON.parse(e.data).error }) // 4. 错误处理:连接断开时自动重连 eventSource.value.onerror = () => { console.warn('SSE connection lost, retrying...') state.status = 'error' } } // 5. 组件卸载时关闭连接 onBeforeUnmount(() => { if (eventSource.value) { eventSource.value.close() } }) </script> <template> <div class="agent-container"> <div v-for="step in state.steps" :key="step.step_id" class="step-card"> <div class="step-header"> <span class="step-id">{{ step.step_id.slice(0, 8) }}</span> <span class="step-time">{{ new Date(step.timestamp).toLocaleTimeString() }}</span> </div> <div class="step-content"> <div v-if="step.thought" class="thought">💭 {{ step.thought }}</div> <div v-if="step.action" class="action">🛠️ {{ step.action }}({{ JSON.stringify(step.action_input) }})</div> <div v-if="step.observation" class="observation">🔍 {{ step.observation }}</div> </div> </div> </div> </template>注意事项:Vue 3 的 Volar 插件(Vue Language Features)是必须启用的。它提供 TypeScript 类型推导,当
state.steps被声明为ReActStep[]时,Volar 能在<div v-for="step in state.steps">中自动识别step的类型,避免step.thought报错。如果禁用 Volar,IDE 会提示 “Property 'thought' does not exist on type 'unknown'”,这是新手最常见的困惑点,根源不是代码错,而是插件没开。
3.3 CLI 工具:轻量、可靠、可调试的入口
CLI 的价值在于“零依赖、秒启动、可脚本化”。它不应依赖任何 Web 框架,只用标准库argparse和requests:
#!/usr/bin/env python3 # cli.py import argparse import json import sys import time from typing import Dict, Any def main(): parser = argparse.ArgumentParser(description="Run ReAct Agent via CLI") parser.add_argument("--task", "-t", required=True, help="Task description") parser.add_argument("--api-url", default="http://localhost:8000", help="FastAPI base URL") parser.add_argument("--timeout", type=int, default=300, help="Request timeout in seconds") args = parser.parse_args() # 构造请求 url = f"{args.api_url}/v1/agent/run" payload = {"task": args.task} try: # 使用 requests.stream=True 获取流式响应 with requests.post(url, json=payload, stream=True, timeout=args.timeout) as r: if r.status_code != 200: print(f"Error: {r.status_code} {r.text}") sys.exit(1) # 解析 SSE 流 for line in r.iter_lines(): if not line: continue # SSE 格式:event: step\ndata: {...}\n\n if line.startswith(b"event:"): event_type = line[6:].strip().decode() elif line.startswith(b"data:"): data = line[5:].strip().decode() if event_type == "step" and data: try: step = json.loads(data) # 格式化输出 print(f"\n[{step['timestamp'][:19]}] Step {step.get('step_id', '')[:8]}") print(f"Thought: {step.get('thought', 'N/A')}") if step.get('action'): print(f"Action: {step['action']}({step['action_input']})") if step.get('observation'): print(f"Observation: {step['observation'][:100]}{'...' if len(step['observation']) > 100 else ''}") except json.JSONDecodeError: pass elif line.startswith(b"event: completed"): print("\n✅ Agent completed.") break except requests.exceptions.Timeout: print("❌ Timeout: Agent execution took too long.") sys.exit(1) except requests.exceptions.ConnectionError: print("❌ Connection Error: Cannot reach API server.") sys.exit(1) if __name__ == "__main__": main()安装方式必须简单:pip install -e .,其中setup.py定义入口:
# setup.py from setuptools import setup, find_packages setup( name="react-agent-cli", version="0.1.0", packages=find_packages(), entry_points={ "console_scripts": [ "react-cli=cli:main", ], }, install_requires=[ "requests>=2.28.0", ], )实操心得:“unable to locate the codex cli binary” 类错误,本质是
PATH未包含pip install -e .生成的可执行文件路径。Linux/macOS 下,pip install -e .会将react-cli符号链接到~/.local/bin/,需确保export PATH="$HOME/.local/bin:$PATH"在~/.bashrc中。Windows 用户则需检查pip show react-agent-cli的Location,并将该路径加入系统环境变量。
4. 全链路实操:从本地开发到生产部署
4.1 本地开发环境搭建:避坑指南
第一步永远是验证 CLI 是否工作。不要急着打开浏览器,先运行:
# 启动 FastAPI uvicorn main:app --reload --host 0.0.0.0 --port 8000 # 在另一个终端运行 CLI react-cli --task "上海今天天气"如果 CLI 输出类似:
[2024-05-20T14:23:11] Step 7a3b9c1d Thought: 我需要查询上海今天的天气情况。 Action: weather_api({"city": "上海"}) Observation: {"temperature": "28°C", "condition": "晴", "humidity": "65%"} [2024-05-20T14:23:12] Step 8e4f1a2b Thought: 天气信息已获取,我可以总结为一句话。 Action: final_answer({"summary": "上海今天天气晴,气温28°C,湿度65%。"}) Observation: N/A ✅ Agent completed.说明后端和 Agent 逻辑完全正常。此时再启动 Vue 3 项目:
cd frontend npm install npm run dev访问http://localhost:5173,输入相同任务,应看到完全一致的步骤流。如果 Web UI 卡住或报错,问题一定在前端或网络层。
常见问题速查表:
现象 可能原因 解决方案 CLI 正常,Web UI 报 net::ERR_CONNECTION_REFUSEDVue 3 开发服务器代理未配置 在 vite.config.ts中添加server.proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true } }CLI 正常,Web UI 收不到任何 step事件CORS 阻止了 SSE 连接 FastAPI 中添加 CORSMiddleware,allow_origins=["*"](开发用),生产环境需指定域名Web UI 显示 stream disconnected before completion: idle timeout waiting for sseNginx 或 Cloudflare 的 timeout 过短 将 proxy_read_timeout 300;加入 Nginx 配置,并重启Vue 控制台报 Uncaught TypeError: Cannot read properties of null (reading 'close')onBeforeUnmount中 eventSource 为 null在 onBeforeUnmount中加if (eventSource.value) eventSource.value.close()
4.2 生产环境部署:Nginx + Uvicorn + PM2
生产环境必须分离关注点:Nginx 处理 SSL、静态资源、反向代理;Uvicorn 运行 FastAPI;PM2 管理 Vue 3 构建产物。
Nginx 配置 (/etc/nginx/sites-available/react-agent):
upstream fastapi_backend { server 127.0.0.1:8000; } server { listen 443 ssl http2; server_name agent.yourdomain.com; ssl_certificate /path/to/fullchain.pem; ssl_certificate_key /path/to/privkey.pem; # 静态资源(Vue 3 build) location / { root /var/www/react-agent-frontend/dist; try_files $uri $uri/ /index.html; } # API 代理 location /api/ { proxy_pass http://fastapi_backend/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 关键:SSE 超时设置 proxy_read_timeout 300; proxy_send_timeout 300; proxy_connect_timeout 300; } }Uvicorn 启动脚本 (start_api.sh):
#!/bin/bash # 使用 systemd 管理,而非裸跑 sudo systemctl stop react-api sudo systemctl start react-apisystemd 服务 (/etc/systemd/system/react-api.service):
[Unit] Description=React Agent API After=network.target [Service] Type=simple User=www-data WorkingDirectory=/opt/react-agent-backend ExecStart=/usr/local/bin/uvicorn main:app --host 127.0.0.1:8000 --workers 4 --timeout-keep-alive 300 Restart=always RestartSec=10 [Install] WantedBy=multi-user.targetVue 3 构建与部署:
# 在 frontend 目录下 npm run build # 将 dist/ 目录内容复制到 /var/www/react-agent-frontend/dist sudo cp -r dist/* /var/www/react-agent-frontend/dist/ sudo chown -R www-data:www-data /var/www/react-agent-frontend实操心得:Uvicorn 的
--workers 4不是越多越好。实测中,worker 数 = CPU 核心数 + 1 是最佳平衡点。过多 worker 会导致内存占用飙升,因为每个 worker 都加载一份 LLM 模型(如果用本地模型)。我们线上用 4 核服务器,--workers 4时内存占用 1.2GB,--workers 8时飙升至 2.8GB,但 QPS 反而下降 15%,原因是模型加载竞争加剧。
4.3 审计日志系统:让每一步都可追溯
可审核性最终体现在日志上。我们用 PostgreSQL 存储三张表:
sessions:id,created_at,user_ip,task_summaryagent_steps:id,session_id,step_id,timestamp,thought,action,action_input,observation,is_finaltool_calls:id,step_id,tool_name,request_body,response_body,duration_ms,status_code
关键是在 FastAPI 的log_audit_event函数中,使用AsyncSession异步写入:
async def log_audit_event( db: AsyncSession, session_id: str, event_type: str, payload: Dict[str, Any] ): # 使用 insert().values() 避免 ORM 开销 stmt = insert(AuditLog).values( session_id=session_id, event_type=event_type, payload=payload, created_at=datetime.now() ) await db.execute(stmt) await db.commit()审计日志的查询接口/api/v1/audit/{session_id}返回结构化 JSON,供 BI 工具或人工审计:
{ "session_id": "abc123", "task": "上海今天天气", "steps": [ { "step_id": "7a3b9c1d", "thought": "我需要查询上海今天的天气情况。", "action": "weather_api", "action_input": {"city": "上海"}, "observation": "{\"temperature\": \"28°C\", \"condition\": \"晴\"}", "timestamp": "2024-05-20T14:23:11.123Z" } ] }注意事项:审计日志必须加密存储敏感字段。
action_input中的 API key、observation中的用户手机号等,需用 AES-256 加密后再存入数据库。密钥由 KMS(Key Management Service)托管,应用启动时动态获取,绝不硬编码。
5. 常见问题深度排查与独家避坑技巧
5.1 “SSE 流中断” 的 7 种根因与修复方案
SSE 中断是本项目最高频问题,表面报错都是stream disconnected before completion: idle timeout waiting for sse,但背后原因各异:
Uvicorn keep-alive timeout 过短
默认--timeout-keep-alive 5秒。Agent 思考超 5 秒即断。
✅ 修复:启动时加--timeout-keep-alive 300。Nginx proxy_read_timeout 未设置
Nginx 默认 60 秒,与 Uvicorn 不匹配。
✅ 修复:Nginx 配置中显式proxy_read_timeout 300;。Cloudflare 的 “Always Online” 功能干扰
Cloudflare 会缓存 SSE 连接,导致流中断。
✅ 修复:在 Cloudflare 规则中,对/api/v1/agent/run路径禁用缓存,设置Cache Level: Bypass。浏览器自身 idle timeout
Chrome 对 SSE 连接有隐式 300 秒 idle timeout。
✅ 修复:服务端定期发送data: \n\n心跳(每 250 秒一次),保持连接活跃。FastAPI 中间件阻塞流式响应
某些中间件(如GZipMiddleware)会缓冲响应体,破坏 SSE 的逐块传输。
✅ 修复:禁用 GZip 中间件,或在StreamingResponse中显式headers={"Content-Encoding": "identity"}。Vue 3 的 EventSource 未处理重连
EventSource默认重试 3 秒,但若服务端 3 秒内未恢复,会放弃。
✅ 修复:在eventSource.onerror中手动location.reload()或弹窗提示。LLM 调用超时未被捕获
OpenAI API 超时,但 Agent 未抛出异常,导致流停滞。
✅ 修复:在AuditableReActAgent中,为每个工具调用设置timeout=30,超时则raise TimeoutError。
5.2 Vue 3 中 SSE 的内存泄漏陷阱
Vue 3 的onBeforeUnmount是清理连接的黄金位置,但有一个致命陷阱:如果组件被v-if隐藏而非销毁,onBeforeUnmount不会触发。例如,用v-if="showAgent"控制 Agent 组件显示,当showAgent=false时,组件被移除,onBeforeUnmount触发;但如果用v-show,组件只是display:none,onBeforeUnmount永远不执行,EventSource 连接持续存在。
独家技巧:在
onMounted中,同时监听window.beforeunload事件,作为兜底清理:onMounted(() => { const cleanup = () => { if (eventSource.value) { eventSource.value.close() } } window.addEventListener('beforeunload', cleanup) // 同时在 onBeforeUnmount 中调用 cleanup })
5.3 FastAPI 的 CORS 配置雷区
fastapi cors搜索结果常推荐add_middleware(CORSMiddleware),但生产环境必须谨慎:
allow_origins=["*"]在开发环境 OK,但生产环境会暴露Access-Control-Allow-Credentials: true,导致浏览器拒绝响应(CORS 安全策略)。- 正确做法是精确指定域名:
allow_origins=["https://agent.yourdomain.com"]。 - 更关键的是,SSE 连接需要
allow_credentials=True,否则 Cookie 认证失效。此时allow_origins不能为["*"],必须为具体域名。
app.add_middleware( CORSMiddleware, allow_origins=["https://agent.yourdomain.com"], # 严格指定 allow_credentials=True, # 启用 Cookie allow_methods=["*"], allow_headers=["*"], )5.4 CLI 的跨平台兼容性问题
react-cli在 Windows 上常报unable to locate the codex cli binary,根源是 Windows 的PATH解析与 Unix 不同:
- Unix 系统中,
pip install -e .生成的可执行文件在~/.local/bin/,PATH包含此路径即可。 - Windows 中,
pip install -e .会创建.exe文件在Scripts目录(如C:\Users\Name\AppData\Roaming\Python\Python39\Scripts),但此路径未必在系统PATH中。
✅ 终极解决方案:不依赖PATH,而是用 Python 的subprocess直接调用:
# 在 setup.py 的 entry_points 中,改为 "console_scripts": [ "react-cli=cli:main", ], # 然后在代码中,用绝对路径调用 import subprocess import sys subprocess.run([sys.executable, "-m", "cli", "--task", "test"])这样无论在哪种系统,都能准确定位模块。
6. 项目延展与个人经验总结
这个 Week 11