我是张大鹏,做了十多年人工智能,带过不少项目。说实话,最难的不是把功能做出来,是在需求变化时让架构跟得上。最近如意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:
python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
import threading
from server.config import load_server_config
from server.routes import chat, system, llm
from agentmain import GeneraticAgent
from storage.chat import make_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=["*"],
)
# 全局状态管理
class AppState:
def __init__(self):
self.agent: GeneraticAgent | None = None
self.chat_repo = None
self.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")
async def startup_event():
"""应用启动时初始化Agent"""
agent = GeneraticAgent()
chat_repo = make_chat_repo()
agent.set_chat_persistence(chat_repo)
# 后台线程运行Agent
threading.Thread(target=agent.run, daemon=True).start()
app.state.app_state.agent = agent
app.state.app_state.chat_repo = chat_repo
print(f"[Server] Agent 初始化完成,当前模型: {agent.get_llm_name()}")
关键点:
- Agent运行在后台线程,主线程处理HTTP请求,互不阻塞
- 全局状态 通过
app.state共享,避免全局变量污染 - CORS全开放,方便前端开发和跨域调用
3.2 配置分离
服务端配置独立为 config/server.yaml:
yaml
server:
host: "0.0.0.0"
port: 8000
workers: 1
cors:
allow_origins: ["*"]
allow_credentials: true
allow_methods: ["*"]
allow_headers: ["*"]
llm:
default_provider: "kimi"
fallback_providers: ["openai", "deepseek"]
storage:
chat_db_path: "data/chat.db"
log_level: "INFO"
配置加载用标准YAML解析,约30行代码:
python
import yaml
from pathlib import Path
def load_server_config(config_path: str | None = None) -> dict:
if config_path is None:
config_path = "config/server.yaml"
with open(Path(config_path), "r", encoding="utf-8") as f:
return yaml.safe_load(f)
3.3 Chat路由设计
聊天是核心功能,支持两种模式:
同步模式(REST API):
python
@router.post("/send")
async def send_message(request: SendMessageRequest) -> dict[str, str]:
state = app.state.app_state
if state.agent is None:
raise HTTPException(status_code=503, detail="Agent未初始化")
conv_id = request.conversation_id or str(uuid.uuid4())
task_id = str(uuid.uuid4())
# 异步处理任务
async def process_task() -> None:
display_queue = state.agent.put_task(request.message, source="api")
while True:
try:
chunk = display_queue.get(timeout=0.1)
if chunk is None:
break
# 收集响应片段
state.active_tasks[task_id]["chunks"].append(str(chunk))
except queue.Empty:
if not state.agent.is_running:
break
await asyncio.sleep(0.05)
asyncio.create_task(process_task())
return {"task_id": task_id, "conversation_id": conv_id}
流式模式(WebSocket):
python
@router.websocket("/ws/{task_id}")
async def websocket_endpoint(websocket: WebSocket, task_id: str):
await websocket.accept()
state = app.state.app_state
try:
while True:
if task_id in state.active_tasks:
task = state.active_tasks[task_id]
# 发送已收集的chunks
chunks = task["chunks"]
for chunk in chunks:
await websocket.send_text(chunk)
if task["status"] == "completed":
await websocket.send_text("[DONE]")
break
await asyncio.sleep(0.1)
except WebSocketDisconnect:
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的响应分为三个阶段:
python
# thinking 阶段
🔍 正在分析问题...
# summary 阶段
💡 关键结论:建议采用方案B,因为...
# answer 阶段
详细解释...
代码示例...
Rich的 Live 组件让流式更新很流畅:
python
from rich.live import Live
from rich.panel import Panel
from rich.markdown import Markdown
with Live(console=console, refresh_per_second=10) as live:
for chunk in stream_response():
if chunk["type"] == "thinking":
content = f"🔍 {chunk['content']}"
elif chunk["type"] == "summary":
content = f"💡 {chunk['content']}"
else:
content = chunk["content"]
live.update(Panel(Markdown(content), title="如意Agent"))
4.3 CLI命令结构
用 Typer 构建命令行入口:
python
import typer
from rich.console import Console
app = typer.Typer(help="如意Agent 终端客户端")
console = Console()
@app.command()
def chat(
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()
def status(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:项目地址(含完整源码)
相关推荐: