我是张大鹏,做了十多年人工智能,带过不少项目。说实话,最难的不是把功能做出来,是在需求变化时让架构跟得上。最近如意Agent经历了一次彻底的架构转型——从桌面GUI全面转向终端版,采用前后端分离架构。本文记录这次重构的完整思路和实现细节。
一、为什么要推倒重来
如意Agent最初是基于PySide6的桌面应用。PySide6确实好用,信号槽机制成熟,QSS样式灵活,我们甚至做了8套主题皮肤。
但跑了几个月后,问题逐渐暴露:
| 问题 | 具体表现 | 影响 |
|---|---|---|
| 打包体积 | PyInstaller + Qt 依赖,单文件 180MB+ | 分发困难,更新成本高 |
| 跨平台 | Windows/Mac/Linux 表现不一致 | 维护三套UI代码 |
| 远程使用 | 必须在本地运行,无法远程调用 | 服务器场景完全不可用 |
| 测试成本 | GUI自动化测试脆弱,CI/CD 难集成 | 每次发版手工验证 |
| 资源占用 | 运行时内存 200MB+ | 低配机器卡顿明显 |
最致命的是部署场景。有用户想在服务器上跑如意Agent作为后台服务,但桌面GUI在 headless 环境下直接报错。我们不得不告诉他们:“先装个桌面环境。”
这显然不合理。
二、新架构的核心思路
重构目标很明确:让Agent回归服务本质,UI只是多种消费方式之一。
新架构采用“共享后端 + 多端前端”模式:
┌─────────────────┐ HTTP/WebSocket ┌─────────────────┐ │ ruyi-cli │ ◄──────────────────────► │ ruyi-server │ │ (Rich终端UI) │ │ (FastAPI服务) │ └─────────────────┘ └────────┬────────┘ │ ┌────────────────────────────┼────┐ │ │ │ ┌─────┴─────┐ ┌──────┴───┐ │ │ Web │ │ Mobile │ │ │ (Vue3) │ │ (未来) │ │ └───────────┘ └──────────┘ │ │ ┌─────────────┴─────┐ │ Core Agent │ │ (agentmain.py) │ └───────────────────┘技术选型
| 层级 | 技术 | 选型理由 |
|---|---|---|
| 后端服务 | FastAPI + Uvicorn | 异步支持好,自动API文档,WebSocket原生支持 |
| 终端UI | Rich + Typer | Python终端渲染天花板,比很多GUI还好看 |
| 配置管理 | YAML | 人机双友好,注释支持,层级清晰 |
| 进程通信 | HTTP + WebSocket | 松耦合,支持远程,调试方便 |
三、后端服务层实现
后端是独立进程,负责承载核心Agent和对外提供API。
3.1 FastAPI应用入口
ruyi-server/src/server/app.py:
fromfastapiimportFastAPIfromfastapi.middleware.corsimportCORSMiddlewareimportuvicornimportthreadingfromserver.configimportload_server_configfromserver.routesimportchat,system,llmfromagentmainimportGeneraticAgentfromstorage.chatimportmake_chat_repo# 加载配置config=load_server_config()# 创建FastAPI应用app=FastAPI(title="如意Agent API",version="0.1.5",description="如意Agent 后端服务 API")# CORS配置,支持跨域调用app.add_middleware(CORSMiddleware,allow_origins=config["cors"]["allow_origins"],allow_credentials=True,allow_methods=["*"],allow_headers=["*"],)# 全局状态管理classAppState:def__init__(self):self.agent:GeneraticAgent|None=Noneself.chat_repo=Noneself.active_tasks:dict={}app.state.app_state=AppState()# 注册路由app.include_router(chat.router)app.include_router(system.router)app.include_router(llm.router)@app.on_event("startup")asyncdefstartup_event():"""应用启动时初始化Agent"""agent=GeneraticAgent()chat_repo=make_chat_repo()agent.set_chat_persistence(chat_repo)# 后台线程运行Agentthreading.Thread(target=agent.run,daemon=True).start()app.state.app_state.agent=agent app.state.app_state.chat_repo=chat_repoprint(f"[Server] Agent 初始化完成,当前模型:{agent.get_llm_name()}")关键点:
- Agent运行在后台线程,主线程处理HTTP请求,互不阻塞
- 全局状态通过
app.state共享,避免全局变量污染 - CORS全开放,方便前端开发和跨域调用
3.2 配置分离
服务端配置独立为config/server.yaml:
server:host:"0.0.0.0"port:8000workers:1cors:allow_origins:["*"]allow_credentials:trueallow_methods:["*"]allow_headers:["*"]llm:default_provider:"kimi"fallback_providers:["openai","deepseek"]storage:chat_db_path:"data/chat.db"log_level:"INFO"配置加载用标准YAML解析,约30行代码:
importyamlfrompathlibimportPathdefload_server_config(config_path:str|None=None)->dict:ifconfig_pathisNone:config_path="config/server.yaml"withopen(Path(config_path),"r",encoding="utf-8")asf:returnyaml.safe_load(f)3.3 Chat路由设计
聊天是核心功能,支持两种模式:
同步模式(REST API):
@router.post("/send")asyncdefsend_message(request:SendMessageRequest)->dict[str,str]:state=app.state.app_stateifstate.agentisNone:raiseHTTPException(status_code=503,detail="Agent未初始化")conv_id=request.conversation_idorstr(uuid.uuid4())task_id=str(uuid.uuid4())# 异步处理任务asyncdefprocess_task()->None:display_queue=state.agent.put_task(request.message,source="api")whileTrue:try:chunk=display_queue.get(timeout=0.1)ifchunkisNone:break# 收集响应片段state.active_tasks[task_id]["chunks"].append(str(chunk))exceptqueue.Empty:ifnotstate.agent.is_running:breakawaitasyncio.sleep(0.05)asyncio.create_task(process_task())return{"task_id":task_id,"conversation_id":conv_id}流式模式(WebSocket):
@router.websocket("/ws/{task_id}")asyncdefwebsocket_endpoint(websocket:WebSocket,task_id:str):awaitwebsocket.accept()state=app.state.app_statetry:whileTrue:iftask_idinstate.active_tasks:task=state.active_tasks[task_id]# 发送已收集的chunkschunks=task["chunks"]forchunkinchunks:awaitwebsocket.send_text(chunk)iftask["status"]=="completed":awaitwebsocket.send_text("[DONE]")breakawaitasyncio.sleep(0.1)exceptWebSocketDisconnect:print(f"[WebSocket] 客户端断开:{task_id}")WebSocket的设计很务实:Agent内部用Queue生产数据,WebSocket循环消费并推送给客户端。不追求零延迟,保证不丢消息、不乱序。
四、终端客户端实现
终端版不是简陋的print,而是基于Rich的现代化TUI。
4.1 为什么选Rich
Rich的能力远超预期:
- Markdown渲染:代码高亮、表格、引用块,全部原生支持
- Panel布局:消息气泡、系统提示,用Panel轻松实现
- Spinner/Progress:Agent思考时显示动画,体验接近GUI
- 颜色主题:256色支持,暗色主题下的显示效果非常舒服
4.2 三段式流式显示
这是终端版最核心的UX创新。Agent的响应分为三个阶段:
# thinking 阶段🔍 正在分析问题...# summary 阶段💡 关键结论:建议采用方案B,因为...# answer 阶段详细解释...代码示例...Rich的Live组件让流式更新很流畅:
fromrich.liveimportLivefromrich.panelimportPanelfromrich.markdownimportMarkdownwithLive(console=console,refresh_per_second=10)aslive:forchunkinstream_response():ifchunk["type"]=="thinking":content=f"🔍{chunk['content']}"elifchunk["type"]=="summary":content=f"💡{chunk['content']}"else:content=chunk["content"]live.update(Panel(Markdown(content),title="如意Agent"))4.3 CLI命令结构
用Typer构建命令行入口:
importtyperfromrich.consoleimportConsole app=typer.Typer(help="如意Agent 终端客户端")console=Console()@app.command()defchat(server:str=typer.Option("http://localhost:8000","--server","-s"),model:str=typer.Option(None,"--model","-m")):"""启动交互式聊天会话"""client=RuyiClient(base_url=server)session=ChatSession(client,model=model)session.run()@app.command()defstatus(server:str=typer.Option("http://localhost:8000","--server","-s")):"""查看Agent运行状态"""client=RuyiClient(base_url=server)info=client.get_status()console.print(f"模型:{info['model']}")console.print(f"状态:{info['status']}")if__name__=="__main__":app()五、重构过程中的关键决策
5.1 为什么不是TUI框架(Textual)?
Textual确实更强大,但我们评估后放弃:
| 维度 | Rich | Textual |
|---|---|---|
| 学习成本 | 低(熟悉print即可上手) | 高(需要理解组件树、事件循环) |
| 调试难度 | 低(print可辅助调试) | 高(屏幕刷新会覆盖print) |
| 灵活性 | 高(自由控制输出) | 中(受框架约束) |
| 包体积 | 小(核心仅依赖) | 大(额外依赖) |
Rich的"增强版print"哲学更符合我们的需求:渐进增强,随时可回退到基础模式。
5.2 进程间通信为什么不用gRPC?
gRPC性能更好,但HTTP/JSON在调试和开发体验上碾压:
curl直接测试API- 浏览器打开
http://localhost:8000/docs看Swagger文档 - 错误信息JSON可直接阅读
对于AI Agent场景,开发效率 > 极致性能。瓶颈在LLM API调用,不在内部通信。
5.3 保留的核心资产
重构不是重写,核心层完全保留:
agentmain.py—— Agent主逻辑llmcore.py—— LLM路由与调用agent_loop.py—— 执行循环storage/—— 持久化层(刚做完chat.db迁移)logstack/—— 结构化日志memory/—— 记忆系统
删除的只有UI层:src/desktop/、src/pet/、src/frontends/。
六、重构收益
| 指标 | 重构前(桌面版) | 重构后(终端版) | 变化 |
|---|---|---|---|
| 打包体积 | 180MB+ | 15MB | -92% |
| 启动时间 | 3-5秒 | <1秒 | -80% |
| 内存占用 | 200MB+ | 40MB | -80% |
| CI/CD集成 | 困难 | 原生支持 | 质变 |
| 远程部署 | 不支持 | 开箱即用 | 质变 |
| 跨平台 | 需分别测试 | Python标准库 | 质变 |
最意外的收获是测试覆盖率。终端版可以全量跑E2E测试,桌面版只能测核心逻辑。重构后测试从 600+ 提升到785 passed,0 skipped。
总结
| 维度 | 内容 |
|---|---|
| 核心思路 | 共享后端 + 多端前端,Agent回归服务本质 |
| 关键技术 | FastAPI(后端)、Rich+Typer(终端)、YAML(配置) |
| 关键决策 | HTTP/JSON优于gRPC,Rich优于Textual,保留核心层 |
| 注意事项 | 终端版适合服务器/开发场景,桌面版可基于Web技术重建 |
这次重构验证了一个原则:架构要服务于场景,不要服务于技术栈本身。PySide6不是不好,是不适合如意Agent当前的发展阶段。当用户从"本地尝鲜"转向"生产部署"时,轻量、可远程、可集成的架构才是正解。
参考资料:
- FastAPI官方文档
- Rich文档
- Typer文档
- 如意Agent终端版实施计划:
docs/superpowers/plans/2026-05-06-terminal-version.md
作者:张大鹏
日期:2026-05-06
团队:大鹏AI教育
GitHub:项目地址(含完整源码)
相关推荐:
- 如意Agent六边形架构改造:从单体巨石到端口适配器
- 如意Agent对话持久化与滚动记忆引擎设计