- Add message ID generation for initial HumanMessage creation - Implement message converters for LangChain/OpenAI format interoperability - Update documentation with detailed message format specifications - Refactor AgentService to use centralized message conversion utilities - Ensure tool_calls format compliance with OpenAI API standards
18 KiB
GNS3 Copilot Agent Chat API 设计文档
概述
本文档描述 GNS3 Copilot Chat API 的架构设计和实现方案。该 API 使客户端能够通过 RESTful 接口与 GNS3 Copilot Agent 进行交互,提供流式对话、会话管理、项目拓扑查询等功能。
核心特性
- 项目级隔离:每个 GNS3 项目拥有独立的 Agent 实例和会话存储
- 流式响应:使用 Server-Sent Events (SSE) 实现实时流式输出
- 会话管理:支持会话列表、重命名、删除、历史记录查询
- 统计追踪:自动记录消息数量、LLM 调用次数、Token 使用量
- 用户隔离:每个用户拥有独立的 LLM 配置和会话空间
架构设计
整体架构
Frontend (Web UI)
│
│ SSE Streaming
▼
FastAPI Chat API Routes
│
│ Project-level Agent Management
▼
AgentService (per project)
│
├─ SQLite Checkpointer (project_dir/gns3-copilot/)
│ ├─ checkpoints table (LangGraph state)
│ └─ chat_sessions table (session metadata)
│
└─ LangGraph Agent
├─ llm_call node
├─ should_continue node
└─ tool_node (GNS3 tools)
项目级 Checkpoint 设计
每个 GNS3 项目在项目目录下创建 gns3-copilot/copilot_checkpoints.db SQLite 数据库,包含两张表:
- checkpoints 表(LangGraph 自动管理):存储 Agent 的对话状态和记忆
- chat_sessions 表(自定义):存储会话元数据和统计信息
目录结构:
{project.path}/
├── gns3-copilot/
│ └── copilot_checkpoints.db
├── project-files/
└── project.gns3
设计优势:
- 项目删除时自动清理所有相关数据
- 实现项目级别的会话隔离
- 便于备份和迁移
用户认证信息传递
背景需求
GNS3 Copilot Agent 需要以下信息才能正常工作:
- user_id:获取用户专属的 LLM 配置
- jwt_token:调用 GNS3 API 时进行身份验证
- llm_config:包含 provider、model、api_key 等配置
ContextVars 方案
使用 Python 的 contextvars.ContextVar 在请求作用域内传递数据,避免敏感信息持久化到 checkpoint。
数据流:
1. API 层获取用户信息
├─ 从 FastAPI get_current_active_user 获取 user_id
├─ 从 Authorization header 提取 jwt_token
└─ 从数据库查询 LLM 配置(已解密 API key)
2. 设置 ContextVars(内存临时存储)
├─ set_current_jwt_token(jwt_token)
└─ set_current_llm_config(llm_config)
3. 构建安全的 LangGraph config(仅包含非敏感标识符)
{
"configurable": {
"thread_id": session_id,
"project_id": project_id
},
"metadata": {
"user_id": user_id
}
}
4. LLM 节点从 ContextVars 获取配置
├─ get_current_jwt_token()
└─ get_current_llm_config()
方案优势:
- 敏感数据(JWT token、API key)仅存储在内存中
- 请求结束后自动清理,不会持久化到数据库
- 避免序列化/反序列化开销
- 实现请求级别的数据隔离
会话管理
chat_sessions 表结构
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INTEGER | 主键(自增) |
| thread_id | TEXT | LangGraph thread_id(唯一) |
| user_id | TEXT | 用户 ID |
| project_id | TEXT | GNS3 项目 ID |
| title | TEXT | 会话标题 |
| message_count | INTEGER | 消息数量 |
| llm_calls_count | INTEGER | LLM 调用次数 |
| input_tokens | INTEGER | 输入 token 总数 |
| output_tokens | INTEGER | 输出 token 总数 |
| total_tokens | INTEGER | 总 token 数 |
| last_message_at | TIMESTAMP | 最后消息时间 |
| created_at | TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP | 更新时间 |
| metadata | TEXT | 预留元数据(JSON) |
| stats | TEXT | 额外统计信息(JSON) |
ChatSessionsRepository
提供会话的 CRUD 操作:
- create_session:创建新会话
- get_session_by_thread:根据 thread_id 查询会话
- list_sessions:列出用户的会话(支持过滤和分页)
- update_session:更新会话(支持增量更新计数器)
- delete_session:删除会话及其 checkpoints
- delete_all_sessions:删除项目的所有会话
统计信息自动收集
统计信息在对话过程中实时收集,流结束后一次性更新到 chat_sessions 表。
实现位置:agent_service.py 的 stream_chat 方法
统计逻辑:
-
message_count(消息数量)
- 初始值:1(用户消息)
on_chat_model_end事件:+1(AI 完整回复,不是每个 chunk)on_tool_end事件:+1(每个工具执行结果)
-
llm_calls_count(LLM 调用次数)
- 监听
on_chat_model_start事件 - 每次 LLM 开始生成时 +1
- 监听
-
input_tokens(输入 token)
- 从
on_chat_model_end事件的usage_metadata中提取 - 重要:LangGraph 返回的 input_tokens 已包含对话历史,每次 LLM 调用都会累加之前的对话内容
- 示例:第1次 input=8674,第2次 input=9421(包含第1次对话 8674+675+系统提示词增量)
- 从
-
output_tokens(输出 token)
- 从
on_chat_model_end事件的usage_metadata中提取 - 重要:LangGraph 返回的 output_tokens 也是累加值,包含所有 LLM 调用的输出
- 示例:第1次实际输出=675,第2次实际输出=9,累加后 output=684(675+9)
- 从
-
total_tokens(总 token)
- 计算公式:input_tokens + output_tokens
- 取最后一次 LLM 调用的累加值进行计算
统计示例(真实数据):
- 第1次 LLM 调用(AI 回复):input=8674, output=675
- 第2次 LLM 调用(生成标题):input=9421, output=684(累加值:675+9)
- 最终存储:input_tokens=9421, output_tokens=684, total_tokens=10105
- 说明:LangGraph 已自动累加,代码直接取最后一次值即可
注意事项:
- message_count 统计的是完整消息,不是流式 chunks
- Token 数据依赖 LLM 返回的
usage_metadata,某些模型可能不支持 - 统计数据在流结束后通过
update_session方法增量更新到数据库 - LangGraph 已自动处理 input 和 output 的历史累加,代码使用最后一次 LLM 调用的值
- 消息 ID 处理:创建初始消息时分配 ID(
HumanMessage(id=str(uuid4()))),从 checkpoint 读取的消息如果没有 ID 也会自动生成 - 格式转换:使用
message_converters.py模块处理 LangChain 和 OpenAI 格式之间的转换,确保 tool_calls 格式符合 OpenAI 规范
Title 自动同步
会话标题由 generate_title 节点自动生成,保存在 LangGraph checkpoint 的 conversation_title 字段中。
同步机制:
- 流式 Chat 完成后,从 checkpoint 读取最终 state
- 检查
conversation_title是否有变化 - 如果有变化,更新到
chat_sessions表
优势:
- 避免在节点中直接访问数据库(防止循环依赖)
- 所有数据库更新集中在流结束后
- 逻辑清晰,易于维护
SSE 消息格式
Chat API 使用 Server-Sent Events (SSE) 进行流式传输。
消息类型
| type | 说明 | 包含字段 |
|---|---|---|
| content | AI 文本内容(流式) | content |
| tool_call | 工具调用请求 | tool_call (id, name, arguments) |
| tool_start | 工具开始执行 | tool_name |
| tool_end | 工具执行完成 | tool_name, tool_output |
| error | 错误信息 | error |
| done | 流结束 | session_id |
| heartbeat | 心跳保活 | session_id |
消息示例
// AI 文本流式输出
{"type": "content", "content": "Hello! How can I help"}
// 工具调用
{"type": "tool_call", "tool_call": {"id": "call_123", "function": {"name": "GNS3TopologyTool", "arguments": {"project_id": "xxx"}}}}
// 工具开始
{"type": "tool_start", "tool_name": "GNS3TopologyTool", "session_id": "xxx"}
// 工具完成
{"type": "tool_end", "tool_name": "GNS3TopologyTool", "tool_output": "{...}", "session_id": "xxx"}
// 完成
{"type": "done", "session_id": "xxx"}
// 错误
{"type": "error", "error": "Project not found", "session_id": "xxx"}
心跳机制
作用:防止代理服务器/负载均衡器因超时断开 SSE 连接。
实现:使用 asyncio.wait 设置超时,超时后发送 heartbeat 消息,然后继续等待下一个事件。
前端处理:收到 heartbeat 消息时直接忽略,不渲染任何内容。
API 端点
所有端点都在 /v3/projects/{project_id}/chat/ 路径下。
| 方法 | 端点 | 说明 |
|---|---|---|
| POST | /stream |
流式 Chat(主要接口) |
| GET | /sessions |
列出会话 |
| GET | /sessions/{session_id}/history |
获取会话历史 |
| PATCH | /sessions/{session_id} |
重命名会话 |
| DELETE | /sessions/{session_id} |
删除会话 |
POST /v3/projects/{project_id}/chat/stream
功能:流式对话接口
请求参数:
- message: 用户消息内容
- session_id: 会话 ID(可选,不提供则自动创建新会话)
- stream: 是否启用流式响应(默认 true)
- mode: 交互模式(当前仅支持 "text")
响应:SSE 流,包含多种类型的消息(见上文消息格式)
项目状态检查:只允许项目状态为 "opened" 时进行对话
GET /v3/projects/{project_id}/chat/sessions
功能:列出项目的所有会话
响应:会话列表,包含统计信息(消息数、token 使用量等)
GET /v3/projects/{project_id}/chat/sessions/{session_id}/history
功能:获取会话的完整历史记录
参数:
- session_id: 会话 ID
- limit: 最大消息数量(默认 100)
响应:
- thread_id: 会话 ID
- title: 会话标题
- messages: 消息列表(OpenAI 格式)
- llm_calls: LLM 调用次数
PATCH /v3/projects/{project_id}/chat/sessions/{session_id}
功能:重命名会话
请求参数:
- title: 新标题(1-255 字符)
响应:更新后的会话信息
DELETE /v3/projects/{project_id}/chat/sessions/{session_id}
功能:删除会话及其所有 checkpoint 数据
响应:204 No Content
数据模型
ChatRequest
- message: str - 用户消息内容
- session_id: Optional[str] - 会话 ID(可选)
- stream: bool - 是否流式响应(默认 true)
- mode: Literal["text"] - 交互模式
ChatSession
会话模型,存储会话元数据和统计信息。
基础字段:
- id: 数据库自增 ID
- thread_id: LangGraph thread_id(会话唯一标识)
- user_id: 用户 ID
- project_id: GNS3 项目 ID
- title: 会话标题(自动生成或用户修改)
统计字段:
- message_count: 完整消息数量(用户消息 + AI 回复 + 工具结果)
- llm_calls_count: LLM 总调用次数
- input_tokens: 输入 token 总数(累加所有 LLM 调用)
- output_tokens: 输出 token 总数(累加所有 LLM 调用)
- total_tokens: 总 token 数(input_tokens + output_tokens)
时间字段:
- last_message_at: 最后一条消息的时间戳
- created_at: 会话创建时间
- updated_at: 会话最后更新时间
预留字段:
- metadata: 元数据 JSON 字符串(存储 mode、status、tags 等)
- stats: 额外统计 JSON 字符串(存储工具调用次数等)
ConversationHistory
- thread_id: str - 会话 ID
- title: str - 会话标题
- messages: List[OpenAIMessage] - 消息列表
- created_at: Optional[str] - 创建时间
- updated_at: Optional[str] - 更新时间
- llm_calls: int - LLM 调用次数
OpenAIMessage
OpenAI 兼容的消息模型。
基础字段:
- id: str - 消息唯一标识符(自动生成或从 LangChain 消息继承)
- role: Literal["user", "assistant", "system", "tool"] - 消息角色
- content: str - 消息内容(支持文本、JSON 字符串)
- created_at: str - 创建时间(ISO 8601)
工具相关字段:
- name: Optional[str] - 工具消息名称(tool 消息)
- tool_call_id: Optional[str] - 关联的工具调用 ID(tool 消息)
- tool_calls: Optional[List[OpenAIToolCall]] - 工具调用列表(assistant 消息)
- id: str - 工具调用 ID
- type: Literal["function"] - 固定为 "function"
- function: Dict - 包含 name 和 arguments(dict 或 JSON 字符串)
元数据:
- metadata: Optional[Dict] - 额外的消息元数据
核心组件
Message Converters(消息格式转换)
文件:gns3server/agent/gns3_copilot/utils/message_converters.py
职责:在 LangChain 消息格式和 OpenAI 兼容格式之间进行转换
主要函数:
convert_langchain_to_openai():LangChain → OpenAI 格式convert_openai_to_langchain():OpenAI → LangChain 格式convert_stream_event_to_openai():流事件 → OpenAI SSE 格式
关键转换逻辑:
-
消息 ID 处理
- 如果消息没有 ID,自动生成 UUID
- 确保所有返回的消息都有唯一标识符
-
Tool Calls 格式转换
- LangChain 格式:
{'name': 'xxx', 'args': {...}, 'id': 'yyy', 'type': 'tool_call'} - OpenAI 格式:
{'id': 'yyy', 'type': 'function', 'function': {'name': 'xxx', 'arguments': '{...}'}} - 自动将
args对象转换为 JSON 字符串(如需要)
- LangChain 格式:
-
Content 类型处理
- 支持 string、dict、list 类型
- 非 string 类型自动转换为 JSON 字符串
实现位置:utils/message_converters.py
AgentService
职责:项目级的 Agent 管理服务
主要方法:
stream_chat:流式对话,自动管理会话和统计get_history:获取会话历史list_sessions:列出会话delete_session:删除会话rename_session:重命名会话close:关闭数据库连接
核心流程(stream_chat):
- 初始化 checkpointer 连接(如果未连接)
- 获取或创建 chat session(从
chat_sessions表) - 设置 ContextVars(JWT token、LLM config)
- 构建 LangGraph config
- 创建带 ID 的初始消息:
HumanMessage(content=message, id=str(uuid4())) - 流式执行 Agent,同时收集统计信息
- 流结束后更新会话统计到数据库
- 同步 auto-generated title
统计收集机制(在 stream_chat 中):
- 监听 LangGraph 的
astream_events事件流 - 在事件循环中实时收集统计数据
- 统计逻辑不依赖转换后的 SSE chunk,直接从原始事件获取
关键事件处理:
on_chat_model_start:LLM 调用次数 +1on_chat_model_end:提取 token 使用量(从output.usage_metadata),AI 消息计数 +1on_tool_end:工具消息计数 +1
实现位置:agent_service.py
ProjectAgentManager
职责:全局单例,管理所有项目的 AgentService 实例
方法:
get_agent(project_id, project_path):获取或创建项目的 AgentServiceremove_agent(project_id):移除项目的 AgentServiceclose_all:关闭所有 AgentService
Chat API Routes
文件:gns3server/api/routes/controller/chat.py
路由注册:
router.include_router(
chat.router,
prefix="/{project_id}/chat",
tags=["Chat"]
)
主要端点实现:
- 所有端点都需要用户认证(
get_current_active_user) - 所有端点都检查项目状态是否为 "opened"
- stream 端点使用
StreamingResponse返回 SSE 流
项目生命周期集成
项目打开时
创建或获取 AgentService 实例:
agent_manager = await get_project_agent_manager()
agent_service = await agent_manager.get_agent(project_id, project.path)
项目关闭时
移除 AgentService 实例,释放资源:
agent_manager.remove_agent(project_id)
项目删除时
- 调用
delete_all_sessions(project_id)删除所有会话和 checkpoint 数据 - 移除 AgentService 实例
- 项目目录被删除,数据库文件也被删除
前端集成
useChat Hook
根据 SSE 消息的 type 字段进行不同处理:
| type | 处理逻辑 |
|---|---|
| content | 追加到当前 AI 消息内容 |
| tool_call | 创建 tool_call 类型消息,显示工具调用信息 |
| tool_start | 可选:显示工具开始执行状态 |
| tool_end | 创建 tool_result 类型消息,显示工具执行结果 |
| error | 显示错误信息 |
| done | 标记流结束,停止加载状态 |
| heartbeat | 忽略(保活信号) |
错误处理
- 网络错误:显示重试选项
- LLM 错误:显示错误消息
- 项目未打开:提示用户打开项目
- LLM 未配置:引导用户配置 LLM
安全考虑
用户隔离
- 每个用户只能访问自己的会话
- user_id 存储在 config.metadata 中
- 所有数据库查询都带 user_id 过滤
项目访问控制
- 只允许访问用户有权限的项目
- 项目状态检查:只允许 "opened" 状态的项目使用 Chat
LLM 配置安全
- API key 加密存储在数据库
- 使用 ContextVars 传递,不持久化到 checkpoint
- 请求结束后自动清理内存中的敏感信息
性能优化
数据库连接管理
- 使用 WAL 模式提升并发写入性能
- 项目级连接复用
- 项目切换时自动关闭旧连接
Checkpoint 优化
- LangGraph 自动管理 checkpoints 表
- 定期清理旧 checkpoint(可选)
- 使用索引加速查询(thread_id, user_id + project_id)
统计信息收集与更新
收集机制(在内存中进行):
- 在 SSE 流式传输过程中同步收集统计数据
- 监听 LangGraph 事件流,不增加额外网络开销
- 使用临时变量累加统计值,避免频繁数据库访问
更新策略(流结束后批量写入):
- 流式 Chat 完成后,一次性更新
chat_sessions表 - 使用 SQL 增量更新语法:
message_count = message_count + ? - 单次数据库事务,提交所有统计更新
优势:
- 减少数据库写入次数(N 次事件 → 1 次更新)
- 降低数据库锁竞争
- 提升流式响应的实时性
实现位置:agent_service.py 第 283-294 行
依赖项
langchain>= 0.3.0langgraph>= 0.2.0langchain-corelanggraph-checkpoint-sqlite>= 3.0.1aiosqlite
扩展性
预留字段
metadata(TEXT JSON):存储会话级别的元数据stats(TEXT JSON):存储额外的统计信息
未来可能的扩展
- 多模态支持(图片、文件)
- 语音输入/输出
- 多人协作会话
- 会话分享和导出
- 自定义工具注册