docs: update AI chat API design with detailed statistics collection

Update the AI chat API design documentation to provide comprehensive details about statistics collection during streaming conversations. The documentation now includes:

1. **Enhanced statistics collection logic**:
   - Clarified message_count increments for user messages, AI responses, and tool results
   - Added LLM call tracking via on_chat_model_start events
   - Detailed token counting methodology using LangGraph's usage_metadata

2. **Improved implementation details**:
   - Added specific event handlers for statistics collection
   - Explained LangGraph's cumulative token counting behavior
   - Provided real-world examples of token accumulation

3. **Updated data models**:
   - Enhanced ChatSession model documentation with field descriptions
   - Separated fields into categories (basic, statistics, timestamps, reserved)

4. **Refined architecture documentation**:
   - Added detailed flow for stream_chat method
   - Documented statistics collection mechanism during SSE streaming
   - Explained batch update strategy to reduce database writes

The changes ensure developers understand how conversation statistics are collected, processed, and stored without impacting streaming performance.
This commit is contained in:
YueGuobin 2026-03-04 22:33:28 +08:00
parent 48e2793719
commit 6b73f00281
2 changed files with 158 additions and 52 deletions

View File

@ -139,13 +139,46 @@ GNS3 Copilot Agent 需要以下信息才能正常工作:
### 统计信息自动收集
`stream_chat` 流程中自动追踪:
- 每次用户消息message_count +1
- 每次 AI 响应message_count +1
- 每次工具调用llm_calls_count +1
- LLM 返回的 token 使用量:实时累加
统计信息在对话过程中实时收集,流结束后一次性更新到 `chat_sessions` 表。
流结束后一次性更新到数据库。
**实现位置**`agent_service.py``stream_chat` 方法
**统计逻辑**
1. **message_count消息数量**
- 初始值1用户消息
- `on_chat_model_end` 事件:+1AI 完整回复,不是每个 chunk
- `on_tool_end` 事件:+1每个工具执行结果
2. **llm_calls_countLLM 调用次数)**
- 监听 `on_chat_model_start` 事件
- 每次 LLM 开始生成时 +1
3. **input_tokens输入 token**
- 从 `on_chat_model_end` 事件的 `usage_metadata` 中提取
- **重要**LangGraph 返回的 input_tokens 已包含对话历史,每次 LLM 调用都会累加之前的对话内容
- 示例第1次 input=8674第2次 input=9421包含第1次对话 8674+675+系统提示词增量)
4. **output_tokens输出 token**
- 从 `on_chat_model_end` 事件的 `usage_metadata` 中提取
- **重要**LangGraph 返回的 output_tokens 也是累加值,包含所有 LLM 调用的输出
- 示例第1次实际输出=675第2次实际输出=9累加后 output=684675+9
5. **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 调用的值
### Title 自动同步
@ -279,21 +312,30 @@ Chat API 使用 Server-Sent Events (SSE) 进行流式传输。
### ChatSession
- id: Optional[int] - 数据库 ID
- thread_id: str - Thread/Session ID
- user_id: str - 用户 ID
- project_id: str - 项目 ID
- title: str - 会话标题
- message_count: int - 消息数量
- llm_calls_count: int - LLM 调用次数
- input_tokens: int - 输入 token 数
- output_tokens: int - 输出 token 数
- total_tokens: int - 总 token 数
- last_message_at: Optional[str] - 最后消息时间
- created_at: Optional[str] - 创建时间
- updated_at: Optional[str] - 更新时间
- metadata: Dict - 预留元数据
- stats: Dict - 额外统计信息
会话模型,存储会话元数据和统计信息。
**基础字段**
- 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
@ -329,18 +371,26 @@ Chat API 使用 Server-Sent Events (SSE) 进行流式传输。
- `close`:关闭数据库连接
**核心流程**stream_chat
1. 获取或创建会话
2. 设置 ContextVarsJWT token、LLM config
3. 构建 LangGraph config
4. 流式执行 Agent收集统计信息
5. 流结束后更新会话统计
6. 同步 auto-generated title
1. 初始化 checkpointer 连接(如果未连接)
2. 获取或创建 chat session`chat_sessions` 表)
3. 设置 ContextVarsJWT token、LLM config
4. 构建 LangGraph config
5. 流式执行 Agent同时收集统计信息
6. 流结束后更新会话统计到数据库
7. 同步 auto-generated title
**连接管理**
- 使用 `AsyncSqliteSaver` 作为 checkpointer
- 支持 WAL 模式提升并发性能
- 项目切换时自动关闭旧连接
- 防止连接被垃圾回收(保存引用)
**统计收集机制**(在 `stream_chat` 中):
- 监听 LangGraph 的 `astream_events` 事件流
- 在事件循环中实时收集统计数据
- 统计逻辑不依赖转换后的 SSE chunk直接从原始事件获取
**关键事件处理**
- `on_chat_model_start`LLM 调用次数 +1
- `on_chat_model_end`:提取 token 使用量AI 消息计数 +1
- `on_tool_end`:工具消息计数 +1
**实现位置**`agent_service.py` 第 233-294 行
### ProjectAgentManager
@ -448,10 +498,24 @@ agent_manager.remove_agent(project_id)
- 定期清理旧 checkpoint可选
- 使用索引加速查询thread_id, user_id + project_id
### 统计信息批量更新
### 统计信息收集与更新
- 流结束后一次性更新统计信息
- 避免频繁的数据库写入
**收集机制**(在内存中进行):
- 在 SSE 流式传输过程中同步收集统计数据
- 监听 LangGraph 事件流,不增加额外网络开销
- 使用临时变量累加统计值,避免频繁数据库访问
**更新策略**(流结束后批量写入):
- 流式 Chat 完成后,一次性更新 `chat_sessions`
- 使用 SQL 增量更新语法:`message_count = message_count + ?`
- 单次数据库事务,提交所有统计更新
**优势**
- 减少数据库写入次数N 次事件 → 1 次更新)
- 降低数据库锁竞争
- 提升流式响应的实时性
**实现位置**`agent_service.py` 第 283-294 行
## 依赖项

View File

@ -233,30 +233,72 @@ class AgentService:
# Track statistics for session update
message_count = 1 # User message
llm_calls_count = 0
tool_calls_count = 0
input_tokens = 0
output_tokens = 0
last_message_at = datetime.utcnow().isoformat()
# Track if we've counted the AI response for this turn
ai_response_counted = False
tool_messages_counted = 0
# Stream events
try:
async for event in graph.astream_events(inputs, config=config, version="v2"):
event_type = event.get("event", "")
data = event.get("data", {})
# Track LLM calls and tokens
if event_type == "on_chat_model_start":
llm_calls_count += 1
log.debug("LLM call started, count=%d", llm_calls_count)
elif event_type == "on_chat_model_end":
# Extract token usage from response metadata
# Try multiple possible locations where token usage might be stored
token_info_found = False
# Method 1: response.usage_metadata
response = data.get("response", {})
if hasattr(response, "usage_metadata"):
usage = response.usage_metadata
if usage:
input_tokens += usage.get("input_tokens", 0)
output_tokens += usage.get("output_tokens", 0)
token_info_found = True
# Method 2: output.usage_metadata
if not token_info_found:
output_msg = data.get("output", {})
if hasattr(output_msg, "usage_metadata"):
usage = output_msg.usage_metadata
if usage:
input_tokens += usage.get("input_tokens", 0)
output_tokens += usage.get("output_tokens", 0)
token_info_found = True
# Method 3: Check data directly for token usage fields
if not token_info_found:
if "input_tokens" in data:
input_tokens += data.get("input_tokens", 0)
if "output_tokens" in data:
output_tokens += data.get("output_tokens", 0)
if "input_tokens" in data or "output_tokens" in data:
token_info_found = True
# Count AI response as one message (only once per turn)
if not ai_response_counted:
message_count += 1
ai_response_counted = True
# Track tool messages
elif event_type == "on_tool_end":
message_count += 1 # Tool result message
tool_messages_counted += 1
log.debug("Tool message counted, message_count=%d", message_count)
# Convert event to chunk for SSE streaming
chunk = self._convert_event_to_chunk(event, session_id)
if chunk:
# Track statistics
if chunk.get("type") == "content":
message_count += 1 # AI response
elif chunk.get("type") == "tool_start":
tool_calls_count += 1
elif chunk.get("type") == "tool_end":
message_count += 1 # Tool message
# Track tokens if available
if chunk.get("type") == "content" and "input_tokens" in chunk:
input_tokens += chunk.get("input_tokens", 0)
if chunk.get("type") == "content" and "output_tokens" in chunk:
output_tokens += chunk.get("output_tokens", 0)
log.debug("Yielding chunk: type=%s", chunk.get("type"))
yield chunk
@ -270,8 +312,8 @@ class AgentService:
total_tokens=input_tokens + output_tokens,
last_message_at=last_message_at
)
log.debug("Updated session statistics: thread_id=%s, messages=%d, tokens=%d",
session_id, message_count, input_tokens + output_tokens)
log.info("Session statistics updated: thread_id=%s, messages=%d, llm_calls=%d, tokens=%d+%d=%d",
session_id, message_count, llm_calls_count, input_tokens, output_tokens, input_tokens + output_tokens)
# Sync auto-generated title from checkpoint state
final_state = await graph.aget_state(config)