基于 LangGraph 构建智能分诊系统(三):FastAPI 接口与 Gradio WebUI 实战

告别命令行,给你的 AI Agent 穿上"Web 外衣"。

在第二篇结束时,我们已经拥有了一个功能完备的 LangGraph 应用,它能在终端里进行多轮对话、调用工具、检索档案并保持记忆。但对于普通用户甚至前端开发人员来说,命令行交互并不友好。

今天我们重点攻克两大任务:

  1. 基于 FastAPI 封装异步 API 接口,支持流式输出(SSE)。

  2. 基于 Gradio 快速搭建带用户认证、会话管理和历史记录加载的 Web 界面。


一、FastAPI 接口开发:让后端服务化

将 Agent 封装为 API 服务,可以让前端(Web、App)方便地调用,也便于后续微服务架构的扩展。

1.1 项目启动入口(main.py

我们需要一个统一的启动入口,负责初始化全局依赖(数据库连接池、LLM 实例、工具配置和状态图),并挂载 FastAPI 路由。

python

复制代码
# main.py
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from api.routes import router as api_router
from config import Config
from llms import get_llm
from tools.tool_config import get_tools, ToolConfig
from graph.workflow import create_graph
from db.postgres_store import get_connection_pool

# 全局变量(懒加载)
graph_app = None
llm_chat = None
llm_embedding = None

@asynccontextmanager
async def lifespan(app: FastAPI):
    """应用生命周期管理:启动时初始化全局资源,关闭时清理连接池"""
    global graph_app, llm_chat, llm_embedding
    logging.info("🚀 正在初始化智能分诊系统...")
    
    # 1. 初始化 LLM
    llm_chat, llm_embedding = get_llm()  # 默认使用 qwen
    
    # 2. 初始化工具
    tools = get_tools(llm_embedding)  # 包含检索工具和 multiply
    tool_config = ToolConfig(tools)
    
    # 3. 创建数据库连接池
    pool = get_connection_pool(Config.DB_URI)
    
    # 4. 编译 LangGraph 应用
    graph_app = create_graph(pool, llm_chat, llm_embedding, tool_config)
    
    # 5. 保存可视化图片(便于调试)
    from graph.workflow import save_graph_visualization
    save_graph_visualization(graph_app, "graph.png")
    
    logging.info("✅ 系统初始化完成")
    
    yield  # 应用运行中...
    
    # 关闭时清理连接池
    pool.close_all()
    logging.info("🛑 系统已关闭")

# 创建 FastAPI 应用
app = FastAPI(
    title="智能分诊系统 API",
    description="基于 LangGraph 的医疗分诊 Agent 接口",
    version="1.0.0",
    lifespan=lifespan
)

# 配置 CORS(允许前端跨域访问)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 生产环境请替换为具体域名
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 注册路由
app.include_router(api_router, prefix="/api")

1.2 核心 API 路由实现(chat 接口)

我们要实现一个支持**流式输出(Server-Sent Events)**的对话接口,让前端能像 ChatGPT 一样逐字接收回复。

python

复制代码
# api/routes.py
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import Optional, AsyncGenerator
import json
import asyncio
import logging

router = APIRouter()

class ChatRequest(BaseModel):
    message: str
    thread_id: str          # 对应 LangGraph 的会话 ID
    user_id: str = "default_user"

@router.post("/chat/stream")
async def chat_stream(request: ChatRequest):
    """流式对话接口,返回 text/event-stream"""
    try:
        # 从全局获取编译好的 graph_app
        from main import graph_app
        
        # 输入配置
        config = {
            "configurable": {
                "thread_id": request.thread_id,
                "user_id": request.user_id
            }
        }
        
        # 构建输入消息
        input_data = {
            "messages": [{"role": "user", "content": request.message}]
        }
        
        async def event_generator() -> AsyncGenerator[str, None]:
            """异步生成 SSE 事件流"""
            try:
                # 使用 astream_events 获取细粒度的事件流
                async for event in graph_app.astream_events(input_data, config, version="v2"):
                    kind = event["event"]
                    
                    # 当 LLM 开始生成新 token 时
                    if kind == "on_chat_model_stream":
                        chunk = event["data"]["chunk"]
                        if chunk.content:
                            yield f"data: {json.dumps({'type': 'content', 'content': chunk.content})}\n\n"
                    
                    # 当工具调用完成时
                    elif kind == "on_tool_end":
                        tool_output = event["data"]["output"]
                        yield f"data: {json.dumps({'type': 'tool_result', 'content': str(tool_output)})}\n\n"
                    
                    # 当整个流程结束时
                    elif kind == "on_chain_end" and event["name"] == "LangGraph":
                        yield f"data: {json.dumps({'type': 'done'})}\n\n"
                        
            except Exception as e:
                logging.error(f"流式生成错误: {e}")
                yield f"data: {json.dumps({'type': 'error', 'content': str(e)})}\n\n"
            
            finally:
                # 结束标记
                yield "data: [DONE]\n\n"
        
        return StreamingResponse(
            event_generator(),
            media_type="text/event-stream",
            headers={
                "Cache-Control": "no-cache",
                "Connection": "keep-alive",
                "X-Accel-Buffering": "no",  # 避免 Nginx 缓冲
            }
        )
        
    except Exception as e:
        logging.error(f"请求处理失败: {e}")
        raise HTTPException(status_code=500, detail=str(e))

1.3 历史会话管理接口

为了方便 WebUI 加载历史消息,我们还需要提供查询历史会话的接口。

python

复制代码
@router.get("/history/{thread_id}")
async def get_history(thread_id: str, user_id: str = "default_user"):
    """获取指定会话的历史消息"""
    from main import graph_app
    config = {"configurable": {"thread_id": thread_id, "user_id": user_id}}
    
    # 从检查点获取历史状态
    state = graph_app.get_state(config)
    if not state or not state.values:
        return {"messages": []}
    
    # 提取消息列表并格式化
    messages = []
    for msg in state.values.get("messages", []):
        messages.append({
            "role": msg.type,  # human, ai, tool
            "content": msg.content
        })
    
    return {"thread_id": thread_id, "messages": messages}

小贴士 :启动 main.py 后,访问 http://127.0.0.1:8000/docs 即可看到自动生成的 Swagger API 文档,方便测试。


二、Gradio WebUI:给 AI 穿上一件漂亮的外衣

Gradio 是构建 AI 应用界面最快的 Python 库。我们不仅要做一个简单的聊天框,还要实现用户注册/登录历史会话加载,让体验更接近真实产品。

2.1 用户认证模块

为了区分不同用户的数据(记忆存储以 user_id 为命名空间),我们需要简单的注册/登录功能。这里使用内存字典模拟用户数据(生产环境应替换为数据库)。

python

复制代码
# webui/auth.py
import hashlib
import json
import os

USER_DB_FILE = "users.json"

def hash_password(password: str) -> str:
    return hashlib.sha256(password.encode()).hexdigest()

def load_users():
    if os.path.exists(USER_DB_FILE):
        with open(USER_DB_FILE, "r") as f:
            return json.load(f)
    return {}

def save_users(users):
    with open(USER_DB_FILE, "w") as f:
        json.dump(users, f)

def register(username: str, password: str) -> bool:
    users = load_users()
    if username in users:
        return False
    users[username] = hash_password(password)
    save_users(users)
    return True

def login(username: str, password: str) -> bool:
    users = load_users()
    if username not in users:
        return False
    return users[username] == hash_password(password)

2.2 主界面构建(webui.py

我们需要构建一个包含"登录/注册"视图和"聊天"视图的界面。Gradio 的 Blocksgr.State 非常适合做多页面状态管理。

python

复制代码
# webui.py
import gradio as gr
import requests
import json
import uuid
from auth import login, register

BASE_URL = "http://127.0.0.1:8000/api"

# ---------- 全局状态 ----------
sessions = {}  # {thread_id: [messages]}

# ---------- 核心交互函数 ----------
def chat_response(message, history, thread_id, user_id):
    """调用 FastAPI 流式接口,并逐字返回"""
    if not thread_id:
        thread_id = str(uuid.uuid4())  # 新会话自动生成 thread_id
    
    # 构建请求
    payload = {"message": message, "thread_id": thread_id, "user_id": user_id}
    
    # 流式请求处理
    try:
        with requests.post(f"{BASE_URL}/chat/stream", json=payload, stream=True) as response:
            if response.status_code != 200:
                yield history + [(message, f"❌ 接口错误: {response.status_code}")]
                return
            
            # 解析 SSE 流
            full_response = ""
            for line in response.iter_lines():
                if not line:
                    continue
                line = line.decode('utf-8')
                if line.startswith("data: "):
                    data = line[6:]
                    if data == "[DONE]":
                        break
                    try:
                        event = json.loads(data)
                        if event.get("type") == "content":
                            full_response += event.get("content", "")
                            # 实时更新界面
                            yield history + [(message, full_response)]
                        elif event.get("type") == "tool_result":
                            # 在界面上显示工具调用信息
                            tool_msg = f"🛠️ 工具调用: {event.get('content')}"
                            yield history + [(message, full_response + f"\n\n{tool_msg}")]
                    except json.JSONDecodeError:
                        continue
    
    except Exception as e:
        yield history + [(message, f"⚠️ 连接失败: {str(e)}")]

def load_history(thread_id, user_id):
    """加载指定会话的历史消息"""
    try:
        resp = requests.get(f"{BASE_URL}/history/{thread_id}?user_id={user_id}")
        if resp.status_code == 200:
            data = resp.json()
            messages = data.get("messages", [])
            # 转换为 Gradio 聊天格式 [(user, assistant), ...]
            history = []
            for i in range(0, len(messages)-1, 2):
                if messages[i]["role"] == "human" and messages[i+1]["role"] == "ai":
                    history.append((messages[i]["content"], messages[i+1]["content"]))
            return history, thread_id
    except Exception as e:
        print(f"加载历史失败: {e}")
    return [], thread_id

# ---------- 界面布局 ----------
with gr.Blocks(theme=gr.themes.Soft(), title="智能分诊系统") as demo:
    gr.Markdown("# 🏥 智能分诊系统")
    gr.Markdown("基于 LangGraph 的 AI 医疗助手,支持健康档案检索、计算和记忆。")
    
    # ---------- 登录/注册组件 ----------
    with gr.Row():
        with gr.Column(scale=1):
            username_input = gr.Textbox(label="用户名", placeholder="请输入用户名")
            password_input = gr.Textbox(label="密码", placeholder="请输入密码", type="password")
            login_btn = gr.Button("登录", variant="primary")
            register_btn = gr.Button("注册", variant="secondary")
            status_msg = gr.Textbox(label="状态", interactive=False)
    
    # ---------- 主聊天界面(初始隐藏) ----------
    with gr.Row(visible=False) as chat_row:
        with gr.Column(scale=4):
            # 会话管理
            with gr.Row():
                new_session_btn = gr.Button("➕ 新建会话", variant="primary")
                session_dropdown = gr.Dropdown(label="历史会话", choices=[], interactive=True)
                refresh_btn = gr.Button("🔄 刷新会话列表", variant="secondary")
            
            # 聊天组件
            chatbot = gr.Chatbot(label="对话窗口", height=500)
            msg = gr.Textbox(label="输入消息", placeholder="请输入您的健康问题...")
            clear = gr.Button("🗑️ 清空当前会话")
    
    # ---------- 状态存储 ----------
    user_state = gr.State({"username": None, "thread_id": None})

    # ---------- 事件绑定 ----------
    
    # 登录逻辑
    def do_login(username, password):
        if login(username, password):
            return (
                gr.update(visible=True),  # 显示聊天区
                {"username": username, "thread_id": str(uuid.uuid4())},
                f"✅ 登录成功!欢迎 {username}",
                gr.update(choices=[]),  # 清空下拉框(后续加载)
                []
            )
        else:
            return gr.update(visible=False), {}, "❌ 用户名或密码错误", gr.update(choices=[]), []
    
    login_btn.click(
        do_login,
        inputs=[username_input, password_input],
        outputs=[chat_row, user_state, status_msg, session_dropdown, chatbot]
    )
    
    # 注册逻辑
    def do_register(username, password):
        if register(username, password):
            return "✅ 注册成功,请登录!"
        else:
            return "❌ 用户名已存在,请更换。"
    
    register_btn.click(do_register, inputs=[username_input, password_input], outputs=[status_msg])
    
    # 发送消息
    def send_message(message, history, user_state):
        if not message.strip():
            return history, ""
        username = user_state.get("username")
        thread_id = user_state.get("thread_id")
        if not thread_id:
            thread_id = str(uuid.uuid4())
            user_state["thread_id"] = thread_id
        
        # 调用流式接口
        for updated_history in chat_response(message, history, thread_id, username):
            yield updated_history, ""
    
    msg.submit(send_message, inputs=[msg, chatbot, user_state], outputs=[chatbot, msg])
    
    # 新建会话
    def new_session():
        new_id = str(uuid.uuid4())
        return new_id, [], gr.update(value=None)
    
    new_session_btn.click(new_session, outputs=[user_state, chatbot, session_dropdown])
    
    # 加载历史会话
    def load_selected_session(thread_id, user_state):
        if thread_id:
            user_state["thread_id"] = thread_id
            history, _ = load_history(thread_id, user_state.get("username"))
            return history, thread_id
        return [], None
    
    session_dropdown.change(
        load_selected_session,
        inputs=[session_dropdown, user_state],
        outputs=[chatbot, user_state]
    )

if __name__ == "__main__":
    demo.launch(server_name="0.0.0.0", server_port=7860, share=False)

三、最终效果与测试

3.1 启动顺序

  1. 启动 PostgreSQL 容器(如果还没启动):

    bash

    复制代码
    docker-compose up -d
  2. 启动 FastAPI 后端

    bash

    复制代码
    python main.py
  3. 启动 Gradio WebUI(新开一个终端):

    bash

    复制代码
    python webui.py
  4. 浏览器访问 http://127.0.0.1:7860

3.2 功能验证

测试场景 操作 预期结果
用户注册 输入用户名/密码,点击注册 状态提示"注册成功"
用户登录 输入已注册的账号密码 自动跳转到聊天界面,显示当前用户
基础对话 输入"你好" 系统正常回复(无需工具)
工具调用 输入"3乘以4等于多少" 调用 multiply 工具,返回正确计算结果
知识检索 输入"张三九的健康档案" 调用检索工具,返回档案信息
记忆功能 输入"记住我叫 Kevin" → 新建会话 → 问"我是谁" 第二次对话能识别出"Kevin"
历史会话加载 在"历史会话"下拉框中选中之前会话 聊天窗口自动加载该会话的历史消息
新建会话 点击"新建会话"按钮 清空当前聊天窗口,生成新的 thread_id

四、部署与优化建议

4.1 生产环境注意事项

  • 数据库连接池 :根据并发量调整 psycopg_poolmax_sizetimeout 参数。

  • 敏感信息 :将数据库 URI、API Key 等放入 .env 文件,使用 python-dotenv 加载。

  • 用户认证:当前示例使用内存 + JSON 文件,生产环境应替换为 JWT + PostgreSQL 用户表。

  • 日志归档 :项目已使用 ConcurrentRotatingFileHandler,建议配合定时任务(如 logrotate)进行日志归档清理。

4.2 性能调优

  • 并行工具调用ParallelToolNodemax_workers 可根据服务器 CPU 核数调整(默认 5)。

  • 检索重写次数 :在 route_after_grade 中设置最大重写次数(项目为 3),避免死循环和 Token 浪费。

  • 消息过滤filter_messages 限制保留最近 5 条消息,有效控制上下文长度。


五、系列总结与进阶方向

恭喜你!通过这三篇文章,我们完成了一个可投入实际使用的智能分诊系统。从项目的整体架构,到数据库与 LLM 的配置,再到 LangGraph 状态图的实现,最后封装为 API 并穿上 Web 界面------每一步都是 LangChain/LangGraph 生态中的典型实践。

回顾三篇的核心收获:

  • 第一篇:项目规划、环境搭建、配置管理。

  • 第二篇:工具定义、动态路由、状态图节点实现、记忆持久化。

  • 第三篇:FastAPI 流式接口、Gradio 登录/聊天/历史会话 WebUI。

接下来你可以继续探索的方向:

  1. 多 Agent 协作 :使用 LangGraph 的 SubGraph 实现医生、药师、导诊等多个 Agent 的协同。

  2. 更丰富的工具:接入病历生成、药品查询、预约挂号等真实 API。

  3. 评估与监控:集成 LangSmith 对 Agent 的每一步推理进行追踪和评分。

  4. RAG 增强:使用更先进的混合检索(稠密+稀疏)和重排序(Reranker)提升检索精度。

AI 应用开发是一场马拉松,而 LangGraph 给了我们构建复杂 Agent 工作流的强大武器。希望这个系列能成为你实战路上的一个有力参考。如果在落地过程中遇到任何问题,欢迎随时交流探讨!

Happy Coding!🚀

相关推荐
神奇霸王龙3 小时前
GB/T 46886 闭环屠夫:5 旗舰多模态 LLM 工业质检实测
人工智能·计算机视觉·ai·开源·ai编程·本地部署
南讯股份Nascent3 小时前
洽洽全域会员项目启动会圆满召开
大数据·人工智能
大郭鹏宇3 小时前
基于 LangGraph 构建智能分诊系统(一):项目概述与环境搭建
大数据·人工智能·microsoft·langchain
小林ixn3 小时前
大模型的“高考成绩单”:读懂Benchmark,选对真·生产力模型
人工智能·llm·测试
冬奇Lab3 小时前
AI 评测系列(04):RAG 评测——RAGAS 四指标实战与一个反直觉发现
人工智能·llm·agent
三声三视3 小时前
uni-app 鸿蒙端传参变成 [object Object]?顺着源码追到 ArkTS router 底层才搞明白
人工智能·ai·uni-app·aigc·ai编程·harmonyos
爱写代码的阿森4 小时前
鸿蒙三方库 | harmony-utils之KvUtil键值型数据库操作详解
数据库·华为·harmonyos·鸿蒙·huawei
fthux4 小时前
“装闭”,让装修套路“装”不下去
人工智能·ai·开源·github·open source
andxe4 小时前
安科士 AndXe 技术博客:400G QSFP112 SR4 光模块|AI 算力与超算短距互联最优方案
网络·人工智能·光模块·光通信
Database_Cool_4 小时前
单机 MySQL 迁移到分布式数据库方便吗?阿里云 PolarDB-X 100% MySQL 协议兼容零改造平滑迁移
数据库·分布式·mysql