从PySide6到Rich+FastAPI:如意Agent终端版架构重构全记录

我是张大鹏,做了十多年人工智能,带过不少项目。说实话,最难的不是把功能做出来,是在需求变化时让架构跟得上。最近如意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当前的发展阶段。当用户从"本地尝鲜"转向"生产部署"时,轻量、可远程、可集成的架构才是正解。


参考资料:


作者 :张大鹏
日期 :2026-05-06
团队 :大鹏AI教育
GitHub:项目地址(含完整源码)

相关推荐:

相关推荐
白鹿第一帅1 小时前
TypeScript+React 全栈生态实战:从架构选型到工程落地,告别开发踩坑
mongodb·react.js·架构·typescript·白鹿第一帅·架构选型·工程落地
超梦dasgg2 小时前
java微服务项目的架构和链路串联
java·微服务·架构
旷世奇才李先生2 小时前
Spring Cloud Alibaba 2026微服务全栈实战:服务治理\+流量控制\+分布式事务
分布式·微服务·架构
用户1708542888853 小时前
拒绝重复劳动:我用AI Agent搭了一套全天候自动化系统,附完整代码
架构
twc8293 小时前
从架构视角梳理全链路压测的核心业务链路
java·大数据·软件测试·架构·性能测试·全链路压测
A_QXBlms3 小时前
企微私域新客运营工具技术选型:从架构与性能看最优解
架构·企业微信
熊文豪3 小时前
国产数据库的中流砥柱:KingbaseES 高可用集群架构深度解析
数据库·架构
YJlio3 小时前
8.2Windows 11 如何用 Xbox Game Bar 实时监测电脑性能?CPU、内存、GPU、显存与 FPS 瓶颈判断教程
windows·笔记·学习·chatgpt·架构·电脑·xbox
humcomm4 小时前
全栈开发技术栈的最新进展(2026年视角)
开发语言·架构