docs: translate AI chat API design document to English

Translate the GNS3 Copilot Agent Chat API design document from Chinese to English to improve accessibility for international contributors and align with project documentation standards. The translation covers all sections including overview, core features, architecture design, API endpoints, and response formats.
This commit is contained in:
YueGuobin 2026-03-06 01:14:52 +08:00
parent 8fa6d6810a
commit 216b3d7219
6 changed files with 1226 additions and 1226 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,152 +1,152 @@
# LLM 上下文窗口管理实现文档
# LLM Context Window Management Implementation Document
## 概述
## Overview
本文档说明 GNS3 Copilot 的上下文窗口管理实现机制包括消息裁剪、Token 计数和配置验证。
This document explains the context window management implementation mechanism for GNS3 Copilot, including message trimming, token counting, and configuration validation.
## 实现架构
## Implementation Architecture
### 1. 核心模块
### 1. Core Modules
**文件位置**: `gns3server/agent/gns3_copilot/agent/context_manager.py`
**File Location**: `gns3server/agent/gns3_copilot/agent/context_manager.py`
#### Token 计数策略
#### Token Counting Strategy
系统使用 **tiktoken** 进行 Token 计数context_manager.py:60
The system uses **tiktoken** for token counting (context_manager.py:60):
```python
_tiktoken_encoding = tiktoken.get_encoding("cl100k_base")
```
**必需依赖**
**Required Dependency**:
```bash
pip install tiktoken>=0.8.0
```
如果未安装 tiktoken系统将在启动时抛出 `ModuleNotFoundError`
If tiktoken is not installed, the system will throw a `ModuleNotFoundError` at startup.
#### 关键函数
#### Key Functions
**`count_tokens(text: str) -> int`** (context_manager.py:84-100)
- 使用 tiktoken 准确计数文本的 token 数
- 使用 `cl100k_base` 编码
- 返回精确的 token 数
- Uses tiktoken to accurately count tokens in text
- Uses `cl100k_base` encoding
- Returns the exact token count
**`estimate_tool_tokens(tools: list) -> int`** (context_manager.py:103-169)
- 序列化工具 schema 为 JSON
- 使用 tiktoken 计数工具定义的 token 消耗
- 支持 Pydantic v1/v2 兼容性
- 失败时使用 1000 tokens 的回退值
- Serializes tool schema to JSON
- Uses tiktoken to count token consumption of tool definitions
- Supports Pydantic v1/v2 compatibility
- Falls back to 1000 tokens on failure
**`create_pre_model_hook(...)`** (context_manager.py:195-402)
- 创建预处理函数pre_model_hook
- 在每次 LLM 调用前自动执行:
1. 注入 topology 信息到 system prompt
2. 估算工具定义的 token 消耗
3. 裁剪消息历史以适应上下文限制
- 返回一个可调用的函数,用于准备消息
- Creates a preprocessing function (pre_model_hook)
- Automatically executes before each LLM call:
1. Injects topology information into system prompt
2. Estimates token consumption of tool definitions
3. Trims message history to fit context limits
- Returns a callable function for preparing messages
### 2. 裁剪逻辑详解
### 2. Detailed Trimming Logic
#### 2.1 Token 预算分配
#### 2.1 Token Budget Allocation
当调用 LLM 时,发送的内容包含两部分:
When calling the LLM, the content sent consists of two parts:
```
发送给 LLM 的完整请求:
Complete request sent to LLM:
┌─────────────────────────────────────────────────────────────┐
│ 1. Messages (我们管理的)
│ ├─ SystemMessage: system prompt + topology (模板注入)
│ └─ HumanMessage/AIMessage: 用户消息 / 历史消息
│ 1. Messages (managed by us)
│ ├─ SystemMessage: system prompt + topology (template injection)
│ └─ HumanMessage/AIMessage: user messages / history messages
├─────────────────────────────────────────────────────────────┤
│ 2. Tool Definitions (LangChain 自动添加,不在消息中)
│ 2. Tool Definitions (LangChain adds automatically, not in messages)
│ ├─ Tool 1 schema (name, description, parameters) │
│ ├─ Tool 2 schema │
│ └─ ... (约 500-1500 tokens per tool)
│ └─ ... (about 500-1500 tokens per tool)
└─────────────────────────────────────────────────────────────┘
```
**System Message 结构**
- 使用模板变量 `{{topology_info}}` 动态注入 topology
- System prompt 包含占位符:`"### CURRENT TOPOLOGY\n{{topology_info}}"`
- 如果有 topology替换为实际内容
- 如果没有 topology替换为 `"(No topology information available)"`
**System Message Structure**:
- Uses template variable `{{topology_info}}` to dynamically inject topology
- System prompt contains placeholder: `"### CURRENT TOPOLOGY\n{{topology_info}}"`
- If topology exists, replaces with actual content
- If no topology, replaces with `"(No topology information available)"`
#### 2.2 裁剪流程
#### 2.2 Trimming Process
```
第一步:计算输入预算
Step 1: Calculate Input Budget
┌─────────────────────────────────────────────────────────────┐
│ context_limit: 128,000 tokens (128K) │
│ strategy: balanced (75%) │
│ │
输入预算 = 128 × 1000 × 0.75 = 96,000 tokens
Input budget = 128 × 1000 × 0.75 = 96,000 tokens
└─────────────────────────────────────────────────────────────┘
第二步:减去工具定义
Step 2: Subtract Tool Definitions
┌─────────────────────────────────────────────────────────────┐
输入预算: 96,000 tokens
工具定义: 1,725 tokens
Input budget: 96,000 tokens
Tool definitions: 1,725 tokens
│ │
可用于消息 = 96,000 - 1,725 = 94,275 tokens
Available for messages = 96,000 - 1,725 = 94,275 tokens
└─────────────────────────────────────────────────────────────┘
第三步trim_messages 处理
Step 3: trim_messages Processing
┌─────────────────────────────────────────────────────────────┐
调用 LangChain 的 trim_messages: │
│ - max_tokens = 94,275 (包含 system message)
│ - strategy = "last" (保留最新消息)
│ - token_counter = tiktoken 计数函数
│ - include_system = True (始终保留 system)
Call LangChain's trim_messages: │
│ - max_tokens = 94,275 (includes system message)
│ - strategy = "last" (keep latest messages)
│ - token_counter = tiktoken counting function
│ - include_system = True (always keep system)
│ │
│ trim_messages 会:
│ 1. 保留 SystemMessage (system + topology)
│ 2. 从最新消息开始,保留尽可能多的历史
│ 3. 超出限制时,丢弃最旧的消息
│ trim_messages will:
│ 1. Keep SystemMessage (system + topology)
│ 2. Starting from latest messages, keep as much history
│ 3. When exceeding limit, discard oldest messages
└─────────────────────────────────────────────────────────────┘
```
#### 2.3 裁剪优先级
#### 2.3 Trimming Priority
系统按以下优先级保留内容:
The system preserves content in the following priority order:
| 优先级 | 内容 | 说明 |
|--------|------|------|
| 1⃣ | System Message (system prompt + topology) | 永远保留 |
| 2⃣ | 最新用户消息 | 至少保留最后1条 |
| 3⃣ | 旧对话历史 | 按时间顺序丢弃 |
| Priority | Content | Description |
|----------|---------|-------------|
| 1⃣ | System Message (system prompt + topology) | Never removed |
| 2⃣ | Latest user message | Keep at least the last 1 |
| 3⃣ | Old conversation history | Discarded in chronological order |
**注意**System prompt 和 topology info 通过模板变量合并为一个 SystemMessage无法单独分离。
**Note**: System prompt and topology info are merged into one SystemMessage via template variable and cannot be separated.
#### 2.4 边界情况处理
#### 2.4 Edge Case Handling
| 情况 | 处理方式 |
|------|----------|
| System (包含 topology) > 预算 | 保留完整的 System Message无法分离 system 和 topology |
| Tools > 预算 | ERROR 日志,建议增加 context_limit 或减少工具数量 |
| 历史全被裁剪 | 保留最后1条用户消息 |
| Scenario | Handling |
|----------|----------|
| System (including topology) > budget | Keep complete SystemMessage (cannot separate system and topology) |
| Tools > budget | ERROR log, suggest increasing context_limit or reducing tool count |
| All history trimmed | Keep last 1 user message |
**重要提示**
- 当 system + topology 超出可用预算时,**两者都会被保留**
- 无法只丢弃 topology 而保留 system prompt因为已合并
**Important Notes**:
- When system + topology exceed available budget, **both are preserved**
- Cannot discard only topology while keeping system prompt (already merged)
### 3. 集成到 GNS3 Copilot
### 3. Integration with GNS3 Copilot
**文件位置**: `gns3server/agent/gns3_copilot/agent/gns3_copilot.py`
**File Location**: `gns3server/agent/gns3_copilot/agent/gns3_copilot.py`
#### 实现方式
#### Implementation Method
**关键点**:系统使用**自定义 StateGraph**,不是 LangGraph 的预构建 agent。
**Key Point**: The system uses a **custom StateGraph**, not LangGraph's pre-built agent.
因此,`pre_model_hook` 不能通过 `model.invoke(config={"configurable": {"pre_model_hook": ...}})` 传递。
Therefore, `pre_model_hook` cannot be passed via `model.invoke(config={"configurable": {"pre_model_hook": ...}})`.
**正确的使用方式****直接调用** `pre_hook` 函数准备消息。
**Correct Usage**: **Directly call** the `pre_hook` function to prepare messages.
```python
def llm_call(state: dict, config: RunnableConfig | None = None):
"""LLM decides whether to call a tool or not."""
# 1. 获取 topology 信息
# 1. Get topology information
project_id = config["configurable"].get("project_id")
topology_info = None
if project_id:
@ -155,7 +155,7 @@ def llm_call(state: dict, config: RunnableConfig | None = None):
if topology and "error" not in topology:
topology_info = topology
# 2. 创建 pre_model_hook
# 2. Create pre_model_hook
system_prompt = load_system_prompt()
pre_hook = create_pre_model_hook(
system_prompt=system_prompt,
@ -164,65 +164,65 @@ def llm_call(state: dict, config: RunnableConfig | None = None):
get_tools_func=lambda: tools,
)
# 3. 创建 model with tools
# 3. Create model with tools
model_with_tools = create_base_model_with_tools(tools, llm_config=llm_config)
# 4. ⭐ 关键:直接调用 pre_hook 准备消息
# 4. ⭐ Key: directly call pre_hook to prepare messages
logger.info("Calling pre_hook to prepare %d messages", len(messages))
prepared_state = pre_hook({"messages": messages, "topology_info": topology_info})
prepared_messages = prepared_state["messages"]
# 5. 使用准备好的消息调用 LLM
# 5. Use prepared messages to call LLM
response = model_with_tools.invoke(prepared_messages)
return {"messages": [response], ...}
```
#### 为什么不通过 config 传递?
#### Why Not Pass via Config?
LangGraph`pre_model_hook` 参数仅适用于**预构建的 agent**,不适用于自定义 StateGraph。
LangGraph's `pre_model_hook` parameter only applies to **pre-built agents**, not custom StateGraphs.
| Agent 类型 | pre_model_hook 支持方式 |
| Agent Type | pre_model_hook Support |
|------------|------------------------|
| `create_react_agent` | ✅ 通过 `pre_model_hook` 参数 |
| `chat_agent_executor` | ✅ 通过 `pre_model_hook` 参数 |
| **自定义 StateGraph** | ❌ **不支持**,需要直接调用 |
| `create_react_agent` | ✅ Via `pre_model_hook` parameter |
| `chat_agent_executor` | ✅ Via `pre_model_hook` parameter |
| **Custom StateGraph** | ❌ **Not supported**, need to call directly |
我们的实现使用的是自定义 StateGraph`agent_builder = StateGraph(MessagesState)`),所以必须直接调用 `pre_hook`
Our implementation uses a custom StateGraph (`agent_builder = StateGraph(MessagesState)`), so we must call `pre_hook` directly.
### 4. 执行流程
### 4. Execution Flow
```
用户发送消息
User sends message
llm_call 节点被调用
llm_call node is called
获取 project_id (从 config["configurable"])
Get project_id (from config["configurable"])
调用 GNS3TopologyTool._run(project_id) 获取 topology
Call GNS3TopologyTool._run(project_id) to get topology
存储 topology_info 到 state
Store topology_info to state
创建 pre_model_hook (通过 create_pre_model_hook())
Create pre_model_hook (via create_pre_model_hook())
【关键】直接调用 pre_hook({"messages": messages, "topology_info": topology_info})
├─ 1. 注入 topology 到 system prompt
├─ 2. 估算工具定义 tokens
├─ 3. 调用 trim_messages() 裁剪消息
└─ 4. 返回准备好的消息列表
[Key] Directly call pre_hook({"messages": messages, "topology_info": topology_info})
├─ 1. Inject topology into system prompt
├─ 2. Estimate tool definitions tokens
├─ 3. Call trim_messages() to trim messages
└─ 4. Return prepared message list
使用准备好的消息调用 model.invoke()
Call model.invoke() with prepared messages
返回 LLM 响应
Return LLM response
```
---
## 策略实现
## Strategy Implementation
### Context Strategy Ratios
**定义**context_manager.py:68-72
**Definition** (context_manager.py:68-72):
```python
CONTEXT_STRATEGY_RATIOS = {
@ -232,24 +232,24 @@ CONTEXT_STRATEGY_RATIOS = {
}
```
**默认值**context_manager.py:74
**Default Value** (context_manager.py:74):
```python
DEFAULT_CONTEXT_STRATEGY = "balanced"
```
### 策略对比
### Strategy Comparison
| 策略 | 输入比例 | 输出预留 | 计算公式 |
|------|---------|---------|---------|
| Strategy | Input Ratio | Output Reserved | Calculation Formula |
|----------|-------------|-----------------|---------------------|
| Conservative | 60% | 40% | `context_limit × 1000 × 0.60` |
| Balanced | 75% | 25% | `context_limit × 1000 × 0.75` |
| Aggressive | 85% | 15% | `context_limit × 1000 × 0.85` |
---
## 日志输出
## Log Output
### 正常情况topology 成功注入)
### Normal Case (topology successfully injected)
```
INFO: Calling pre_hook to prepare 1 messages
@ -259,7 +259,7 @@ INFO: Messages prepared: 1 → 2
INFO: LLM call completed: tool_calls=0
```
### 发生裁剪时
### When Trimming Occurs
```
INFO: Calling pre_hook to prepare 50 messages
@ -268,7 +268,7 @@ INFO: Messages trimmed: 50 → 25 msgs. Total: ~82000 tokens + 1725 tools = 8372
INFO: Messages prepared: 50 → 25
```
### topology 为 None 时
### When topology is None
```
INFO: Calling pre_hook to prepare 1 messages
@ -278,24 +278,24 @@ INFO: Context ready: 2 msgs, ~800 tokens + 1725 tools = 2525 / 128K (2.0%), stra
---
## 错误处理
## Error Handling
### tiktoken 未安装
### tiktoken Not Installed
如果 tiktoken 未安装,系统将在启动时抛出错误:
If tiktoken is not installed, the system will throw an error at startup:
```python
ModuleNotFoundError: No module named 'tiktoken'
```
**解决方法**
**Solution**:
```bash
pip install tiktoken>=0.8.0
```
### context_limit 缺失或无效
### context_limit Missing or Invalid
如果 LLM 配置中没有 `context_limit` 或值无效context_manager.py:285-295
If there is no `context_limit` in the LLM configuration or the value is invalid (context_manager.py:285-295):
```python
if "context_limit" not in llm_config:
@ -306,7 +306,7 @@ if not isinstance(limit, int) or limit <= 0:
raise ValueError(f"Invalid context_limit: {limit}")
```
### 裁剪失败
### Trimming Failure
```python
try:
@ -319,8 +319,8 @@ except Exception as e:
---
## 相关源文件
## Related Source Files
- `gns3server/agent/gns3_copilot/agent/context_manager.py` - 上下文管理核心逻辑
- `gns3server/agent/gns3_copilot/agent/gns3_copilot.py` - LLM 调用节点StateGraph
- `gns3server/agent/gns3_copilot/agent/model_factory.py` - 模型创建和工具绑定
- `gns3server/agent/gns3_copilot/agent/context_manager.py` - Context management core logic
- `gns3server/agent/gns3_copilot/agent/gns3_copilot.py` - LLM call node (StateGraph)
- `gns3server/agent/gns3_copilot/agent/model_factory.py` - Model creation and tool binding

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +1,42 @@
# TODO: 修复孤儿 Tool Calls 导致的 Checkpoint 状态不一致
# TODO: Fix Orphan Tool Calls Causing Checkpoint State Inconsistency
## 问题描述
## Problem Description
当 LangGraph agent 在执行过程中异常终止(如服务被强制关闭、进程崩溃等),可能导致 checkpoint 中保存了包含 `tool_calls``AIMessage`,但没有对应的 `ToolMessage`。这种状态不一致会导致后续对话恢复时出现错误。
When the LangGraph agent terminates abnormally during execution (such as forced service shutdown, process crash, etc.), it may result in a checkpoint containing an `AIMessage` with `tool_calls` but no corresponding `ToolMessage`. This state inconsistency can cause errors during subsequent conversation recovery.
### 术语说明
### Terminology
- **孤儿 tool_calls**`AIMessage` 中包含 `tool_calls` 字段,但消息列表中没有对应的 `ToolMessage`
- **Checkpoint**LangGraph 用于持久化对话状态的机制
- **状态不一致**checkpoint 中的消息状态不符合预期的消息对AIMessage + ToolMessage
- **Orphan tool_calls**: `AIMessage` contains `tool_calls` field, but there's no corresponding `ToolMessage` in the message list
- **Checkpoint**: LangGraph's mechanism for persisting conversation state
- **State inconsistency**: Message state in checkpoint doesn't match expected message pairs (AIMessage + ToolMessage)
---
## 触发场景
## Trigger Scenarios
### 场景 1进程异常终止主要问题
### Scenario 1: Process Abnormal Termination (Primary Issue)
```
执行流程:
用户消息 → llm_call → AIMessage(tool_calls) → [Checkpoint 保存]
Execution flow:
User message → llm_call → AIMessage(tool_calls) → [Checkpoint saved]
[进程崩溃/服务关闭]
[Process crash/service shutdown]
tool_node 未执行
tool_node not executed
Checkpoint 中:
- AIMessage ( tool_calls) ✅
- ToolMessage ❌ 缺失
Checkpoint contains:
- AIMessage (has tool_calls) ✅
- ToolMessage ❌ missing
```
**触发条件:**
- LLM 返回包含 tool_calls 的响应
- Checkpoint 已保存 AIMessage
- 在 tool_node 执行前服务被关闭kill -9、Ctrl+C、崩溃等
**Trigger conditions:**
- LLM returns a response containing tool_calls
- Checkpoint has saved AIMessage
- Service is shut down before tool_node execution (kill -9, Ctrl+C, crash, etc.)
### 场景 2达到最大调用次数已处理
### Scenario 2: Maximum Call Count Reached (Already Handled)
当前代码通过 `recursion_limit_continue` 函数在 tool_node **执行后**检查剩余步数:
Current code checks remaining steps after tool_node execution via the `recursion_limit_continue` function:
```python
def recursion_limit_continue(state: MessagesState) -> Literal["llm_call", END]:
@ -48,59 +48,59 @@ def recursion_limit_continue(state: MessagesState) -> Literal["llm_call", END]:
return END
```
**执行流程:**
**Execution flow:**
```
remaining_steps = 5
llm_call → AIMessage(tool_calls) → remaining_steps = 4
should_continue → tool_node(因为有 tool_calls
should_continue → tool_node (because there are tool_calls)
tool_node → ToolMessage → remaining_steps = 3
recursion_limit_continue → remaining_steps < 4 END
```
**结论:** 场景 2 不会产生孤儿 tool_calls因为 tool_node 总是会执行并生成 ToolMessage。
**Conclusion:** Scenario 2 won't produce orphan tool_calls because tool_node always executes and generates a ToolMessage.
---
## 修复方案
## Fix Solution
### 核心思路
### Core Idea
`stream_chat` 开始时,对于已存在的会话,检测并修复孤儿 tool_calls。
At the start of `stream_chat`, for existing sessions, detect and fix orphan tool_calls.
### 修复策略
### Fix Strategy
**策略 A清除 tool_calls推荐**
**Strategy A: Clear tool_calls (Recommended)**
创建一个新的 `AIMessage`,内容与原消息相同,但不包含 `tool_calls` 字段。
Create a new `AIMessage` with the same content as the original message but without the `tool_calls` field.
**优点:**
- 简单、干净
- 不会影响后续对话
- 用户可以重新提问
**Advantages:**
- Simple and clean
- Won't affect subsequent conversation
- User can ask the question again
**缺点:**
- 丢失了 LLM 原本的意图(但已经崩溃了,无法恢复)
**Disadvantages:**
- Loses LLM's original intent (but it already crashed, can't be recovered)
---
## 实现代码
## Implementation Code
### 1. 添加修复方法(`agent_service.py`
### 1. Add Fix Method (`agent_service.py`)
```python
async def _fix_orphan_tool_calls(self, graph, config: dict, session_id: str):
"""
检测并修复孤儿 tool_callsAIMessage有tool_calls但没有对应ToolMessage
Detect and fix orphan tool_calls (AIMessage has tool_calls but no corresponding ToolMessage).
当进程在 tool_node 执行前崩溃时会产生孤儿 tool_calls。
Orphan tool_calls occur when the process crashes before tool_node execution.
使用 LangGraph 的 aupdate_state API安全地创建新的 checkpoint 版本。
Uses LangGraph's aupdate_state API to safely create a new checkpoint version.
"""
try:
# 1. 读取当前状态
# 1. Read current state
state = await graph.aget_state(config)
if not state or not state.values.get("messages"):
return
@ -108,37 +108,37 @@ async def _fix_orphan_tool_calls(self, graph, config: dict, session_id: str):
messages = state.values["messages"]
last_message = messages[-1]
# 2. 检测孤儿 tool_calls
# 2. Detect orphan tool_calls
if not (hasattr(last_message, "tool_calls") and last_message.tool_calls):
return
# 检查是否有对应的 ToolMessage
# Check if there's a corresponding ToolMessage
has_tool_message = any(isinstance(m, ToolMessage) for m in messages)
if has_tool_message:
return
log.warning("检测到孤儿 tool_calls: session=%s, 将清除", session_id)
log.warning("Detected orphan tool_calls: session=%s, will clear", session_id)
# 3. 创建修复后的消息(不含 tool_calls
# 3. Create fixed message (without tool_calls)
from langchain.messages import AIMessage
fixed_message = AIMessage(
content=last_message.content,
id=getattr(last_message, "id", None)
)
# 4. 使用 LangGraph API 更新状态(创建新 checkpoint
# 4. Use LangGraph API to update state (create new checkpoint)
await graph.aupdate_state(config, {"messages": [fixed_message]})
log.info("孤儿 tool_calls 已修复: session=%s", session_id)
log.info("Orphan tool_calls fixed: session=%s", session_id)
except Exception as e:
log.error("修复孤儿 tool_calls 失败: %s", e, exc_info=True)
log.error("Failed to fix orphan tool_calls: %s", e, exc_info=True)
```
### 2. `stream_chat` 中调用(`agent_service.py`
### 2. Call in `stream_chat` (`agent_service.py`)
在获取 graph 之后、开始 stream 之前添加修复逻辑:
Add fix logic after getting the graph and before starting the stream:
```python
async def stream_chat(
@ -151,7 +151,7 @@ async def stream_chat(
mode: str = "text",
llm_config: Optional[Dict[str, Any]] = None,
) -> AsyncGenerator[Dict[str, Any], None]:
# ... 现有代码 ...
# ... existing code ...
# Get or create chat session
repo = ChatSessionsRepository(self._checkpointer_conn)
@ -163,7 +163,7 @@ async def stream_chat(
session = await repo.create_session(...)
log.debug("Created new chat session: thread_id=%s", session_id)
# ... 设置 context variables ...
# ... set context variables ...
# Build config
config = {
@ -187,100 +187,100 @@ async def stream_chat(
# Get the compiled graph
graph = await self._get_graph()
# 🔧 修复状态:对于已存在的会话,检查并修复孤儿 tool_calls
# 🔧 Fix state: for existing sessions, check and fix orphan tool_calls
if not is_new_session:
await self._fix_orphan_tool_calls(graph, config, session_id)
log.debug("LangGraph graph obtained, starting stream")
# ... 继续现有代码 ...
# ... continue existing code ...
```
### 3. 需要添加的 import
### 3. Required Imports
确保 `agent_service.py` 中有以下 import
Ensure `agent_service.py` has the following import:
```python
from langchain.messages import ToolMessage # 用于检测 ToolMessage 类型
from langchain.messages import ToolMessage # For detecting ToolMessage type
```
---
## 对 Checkpoint 数据库的影响
## Impact on Checkpoint Database
### LangGraph Checkpoint 机制
### LangGraph Checkpoint Mechanism
LangGraph 的 checkpoint 是**版本化**的,每次状态更新会创建新记录:
LangGraph checkpoints are **versioned** - each state update creates a new record:
```
checkpoints 表结构:
checkpoints table structure:
- thread_id
- checkpoint_id (递增的版本号)
- checkpoint (序列化的状态数据)
- checkpoint_id (incrementing version number)
- checkpoint (serialized state data)
- metadata
- ...
```
### 安全性分析
### Security Analysis
| 方面 | 影响 | 说明 |
|------|------|------|
| **原始数据** | 保留不变 | `aupdate_state` 创建新版本,不覆盖历史 |
| **数据库结构** | 完全兼容 | 使用 LangGraph 原生 API不会破坏结构 |
| **并发安全** | 内置保护 | LangGraph 有锁机制处理并发访问 |
| **存储开销** | 很小 | 只增加一条 checkpoint 记录(约几 KB |
| **可回滚性** | 支持 | 可回滚到修复前的任何版本 |
| Aspect | Impact | Description |
|--------|--------|-------------|
| **Original data** | Preserved unchanged | `aupdate_state` creates new version, doesn't overwrite history |
| **Database structure** | Fully compatible | Uses LangGraph native API, won't break structure |
| **Concurrency safety** | Built-in protection | LangGraph has locking mechanism for concurrent access |
| **Storage overhead** | Minimal | Only adds one checkpoint record (about a few KB) |
| **Revertibility** | Supported | Can roll back to any version before fix |
### 为什么不直接操作数据库
### Not Direct Database Manipulation
**❌ 危险方式:**
**❌ Dangerous approach:**
```python
# 直接修改数据库 - 破坏性强
# Direct database modification - destructive
await conn.execute(
"UPDATE checkpoints SET checkpoint = ? WHERE ...",
[modified_json]
)
```
**问题:**
- 可能破坏序列化格式
- 不创建新版本,覆盖历史
- 可能导致数据库锁定或损坏
- 违反 LangGraph 的设计原则
**Problems:**
- May break serialization format
- Doesn't create new version, overwrites history
- May cause database locking or corruption
- Violates LangGraph design principles
**✅ 安全方式:**
**✅ Safe approach:**
```python
# 使用 LangGraph 的 aupdate_state
# Use LangGraph's aupdate_state
await graph.aupdate_state(config, {"messages": [fixed_message]})
```
---
## 测试方法
## Testing Methods
### 方法 1模拟崩溃测试推荐
### Method 1: Simulated Crash Test (Recommended)
利用强制关闭服务来模拟崩溃场景:
Simulate crash scenarios by forcibly shutting down the service:
```
步骤:
1. 启动 GNS3 服务
2. 发送一个会触发 tool_calls 的消息(例如查询拓扑)
3. 观察日志,等待看到 AIMessage 返回(有 tool_calls
4. 在 tool_node 执行完成前,强制关闭服务:
- 方式 1: kill -9 <pid>
- 方式 2: Ctrl+C (如果支持)
5. 重启 GNS3 服务
6. 使用同一个 session_id 继续对话
7. 观察日志,应该看到:
- "检测到孤儿 tool_calls: session=xxx, 将清除"
- "孤儿 tool_calls 已修复: session=xxx"
8. 验证对话可以正常进行
Steps:
1. Start GNS3 service
2. Send a message that triggers tool_calls (e.g., query topology)
3. Observe logs, wait for AIMessage return (with tool_calls)
4. Force shutdown service before tool_node completes:
- Method 1: kill -9 <pid>
- Method 2: Ctrl+C (if supported)
5. Restart GNS3 service
6. Continue conversation using same session_id
7. Observe logs, should see:
- "Detected orphan tool_calls: session=xxx, will clear"
- "Orphan tool_calls fixed: session=xxx"
8. Verify conversation can proceed normally
```
### 方法 2单元测试
### Method 2: Unit Tests
直接构造孤儿 tool_calls 状态来测试修复逻辑:
Directly construct orphan tool_calls state to test fix logic:
```python
# tests/test_agent_service.py
@ -290,24 +290,24 @@ from langchain.messages import AIMessage, HumanMessage, ToolMessage
@pytest.mark.asyncio
async def test_fix_orphan_tool_calls():
"""测试孤儿 tool_calls 修复逻辑"""
"""Test orphan tool_calls fix logic"""
from gns3server.agent.gns3_copilot.agent_service import AgentService
# 创建测试用的 agent service
# Create test agent service
service = AgentService("/tmp/test_project")
await service._get_checkpointer()
graph = await service._get_graph()
config = {"configurable": {"thread_id": "test_session"}}
# 构造孤儿状态:先添加正常消息
# Construct orphan state: add normal messages first
await graph.aupdate_state(
config,
{
"messages": [
HumanMessage(content="测试消息", id="msg_1"),
HumanMessage(content="Test message", id="msg_1"),
AIMessage(
content="让我帮你查一下",
content="Let me check for you",
id="msg_2",
tool_calls=[{
"id": "call_123",
@ -315,30 +315,30 @@ async def test_fix_orphan_tool_calls():
"args": {"project_id": "test"}
}]
)
# 注意:没有对应的 ToolMessage
# Note: No corresponding ToolMessage
],
"llm_calls": 1,
"remaining_steps": 20
}
)
# 调用修复逻辑
# Call fix logic
await service._fix_orphan_tool_calls(graph, config, "test_session")
# 验证修复结果
# Verify fix result
state = await graph.aget_state(config)
last_message = state.values["messages"][-1]
# 应该不再有 tool_calls
# Should no longer have tool_calls
assert not hasattr(last_message, "tool_calls") or not last_message.tool_calls
assert last_message.content == "让我帮你查一下"
assert last_message.content == "Let me check for you"
# 清理
# Cleanup
await service.close()
@pytest.mark.asyncio
async def test_no_fix_when_normal():
"""测试正常状态不会被误修复"""
"""Test that normal state isn't incorrectly fixed"""
from gns3server.agent.gns3_copilot.agent_service import AgentService
service = AgentService("/tmp/test_project")
@ -347,14 +347,14 @@ async def test_no_fix_when_normal():
graph = await service._get_graph()
config = {"configurable": {"thread_id": "test_session_2"}}
# 构造正常状态:有完整的 AIMessage + ToolMessage 对
# Construct normal state: complete AIMessage + ToolMessage pair
await graph.aupdate_state(
config,
{
"messages": [
HumanMessage(content="测试消息", id="msg_1"),
HumanMessage(content="Test message", id="msg_1"),
AIMessage(
content="让我帮你查一下",
content="Let me check for you",
id="msg_2",
tool_calls=[{
"id": "call_123",
@ -363,7 +363,7 @@ async def test_no_fix_when_normal():
}]
),
ToolMessage(
content="拓扑信息:...",
content="Topology info: ...",
tool_call_id="call_123",
name="get_topology",
id="msg_3"
@ -374,78 +374,78 @@ async def test_no_fix_when_normal():
}
)
# 记录原始消息数量
# Record original message count
state_before = await graph.aget_state(config)
msg_count_before = len(state_before.values["messages"])
# 调用修复逻辑
# Call fix logic
await service._fix_orphan_tool_calls(graph, config, "test_session_2")
# 验证状态未改变
# Verify state unchanged
state_after = await graph.aget_state(config)
msg_count_after = len(state_after.values["messages"])
assert msg_count_before == msg_count_after # 不应该添加新消息
assert msg_count_before == msg_count_after # Should not add new messages
last_message = state_after.values["messages"][-1]
assert isinstance(last_message, ToolMessage) # 最后一条还是 ToolMessage
assert isinstance(last_message, ToolMessage) # Last is still ToolMessage
# 清理
# Cleanup
await service.close()
```
### 方法 3增强日志和监控
### Method 3: Enhanced Logging and Monitoring
即使无法主动触发,也可以在生产环境验证修复逻辑是否生效:
Even without active triggering, you can verify fix logic works in production:
```python
# 在 _fix_orphan_tool_calls 中添加详细日志
log.warning("检测到孤儿 tool_calls: session=%s", session_id)
log.info("原始消息: tool_calls=%d, content=%s",
# Add detailed logging in _fix_orphan_tool_calls
log.warning("Detected orphan tool_calls: session=%s", session_id)
log.info("Original message: tool_calls=%d, content=%s",
len(last_message.tool_calls),
last_message.content[:100])
log.info("修复后: tool_calls=%d",
log.info("After fix: tool_calls=%d",
len(fixed_message.tool_calls) if hasattr(fixed_message, "tool_calls") else 0)
```
---
## 文件修改清单
## File Modification Checklist
### 需要修改的文件
### Files to Modify
1. **`gns3server/agent/gns3_copilot/agent_service.py`**
- 添加 `_fix_orphan_tool_calls` 方法
- `stream_chat` 方法中调用修复逻辑
- Add `_fix_orphan_tool_calls` method
- Call fix logic in `stream_chat` method
### 需要添加的测试文件(可选)
### Test Files to Add (Optional)
2. **`tests/test_agent_service.py`**(新建或添加到现有测试文件)
- `test_fix_orphan_tool_calls()` - 测试孤儿 tool_calls 修复
- `test_no_fix_when_normal()` - 测试正常状态不被误修复
2. **`tests/test_agent_service.py`** (create new or add to existing test file)
- `test_fix_orphan_tool_calls()` - Test orphan tool_calls fix
- `test_no_fix_when_normal()` - Test normal state isn't incorrectly fixed
---
## 实施步骤
## Implementation Steps
1. ✅ 创建待办文档(当前文档)
2. ⬜ `agent_service.py` 中添加 `_fix_orphan_tool_calls` 方法
3. ⬜ `stream_chat` 中调用修复逻辑
4. ⬜ 使用模拟崩溃方法测试修复效果
5. ⬜ 添加单元测试(可选)
6. ⬜ 更新相关文档(如有必要)
1. ✅ Create TODO document (current document)
2. ⬜ Add `_fix_orphan_tool_calls` method in `agent_service.py`
3. ⬜ Call fix logic in `stream_chat`
4. ⬜ Test fix effect using simulated crash method
5. ⬜ Add unit tests (optional)
6. ⬜ Update related documentation (if necessary)
---
## 相关代码文件
## Related Code Files
- **主要修改文件**: `gns3server/agent/gns3_copilot/agent_service.py`
- **相关文件**: `gns3server/agent/gns3_copilot/agent/gns3_copilot.py`
- **测试文件**: `tests/test_agent_service.py` (待创建)
- **Main modification file**: `gns3server/agent/gns3_copilot/agent_service.py`
- **Related file**: `gns3server/agent/gns3_copilot/agent/gns3_copilot.py`
- **Test file**: `tests/test_agent_service.py` (to be created)
---
## 参考文档
## Reference Documentation
- [LangGraph Checkpointer 文档](https://langchain-ai.github.io/langgraph/concepts/low_level/#checkpointer)
- [LangGraph Checkpointer Documentation](https://langchain-ai.github.io/langgraph/concepts/low_level/#checkpointer)
- [LangGraph State Management](https://langchain-ai.github.io/langgraph/concepts/low_level/#state)
- [GNS3-Copilot AI Chat API 设计](../ai-chat-api-design.md)
- [GNS3-Copilot AI Chat API Design](../ai-chat-api-design.md)

View File

@ -1,69 +1,69 @@
# GNS3 Copilot Tool Response Format Standard
## 概述
## Overview
本文档定义了 GNS3 Copilot 工具的标准响应格式,确保所有工具返回统一的数据结构,便于前端处理和美化显示。
This document defines the standard response format for GNS3 Copilot tools, ensuring all tools return a unified data structure for easy frontend processing and display.
## 标准响应格式
## Standard Response Format
### 顶层结构
### Top-level Structure
所有工具应返回以下标准格式:
All tools should return the following standard format:
```python
{
"success": bool, # 整体操作是否成功
"total": int, # 总操作数量
"successful": int, # 成功数量
"failed": int, # 失败数量
"data": list[dict], # 详细结果列表
"error": str, # 全局错误信息(可选,操作完全失败时)
"metadata": dict # 元数据(可选)
"success": bool, # Whether the overall operation succeeded
"total": int, # Total number of operations
"successful": int, # Number of successful operations
"failed": int, # Number of failed operations
"data": list[dict], # Detailed result list
"error": str, # Global error message (optional, when operation completely fails)
"metadata": dict # Metadata (optional)
}
```
**字段说明**
**Field Descriptions**:
| 字段 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `success` | `bool` | 是 | 整体操作是否成功(`failed == 0` 时为 `True` |
| `total` | `int` | 是 | 处理的项目总数 |
| `successful` | `int` | 是 | 成功的项目数量 |
| `failed` | `int` | 是 | 失败的项目数量 |
| `data` | `list[dict]` | 是 | 每个项目的详细结果 |
| `error` | `str` | 否 | 全局错误消息(当整个操作失败时) |
| `metadata` | `dict` | 否 | 元数据(时间戳、执行时间等) |
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `success` | `bool` | Yes | Whether the overall operation succeeded (True when `failed == 0`) |
| `total` | `int` | Yes | Total number of items processed |
| `successful` | `int` | Yes | Number of successful items |
| `failed` | `int` | Yes | Number of failed items |
| `data` | `list[dict]` | Yes | Detailed results for each item |
| `error` | `str` | No | Global error message (when entire operation fails) |
| `metadata` | `dict` | No | Metadata (timestamp, execution time, etc.) |
### 单个项目格式
### Single Item Format
`data` 数组中的每个项目应遵循以下格式:
Each item in the `data` array should follow this format:
```python
{
"id": str, # 设备/节点/链接 ID
"name": str, # 人类可读的名称
"status": "success" | "failed", # 项目状态
"result": str, # 成功时的结果或输出
"error": str # 失败时的错误信息
"id": str, # Device/node/link ID
"name": str, # Human-readable name
"status": "success" | "failed", # Item status
"result": str, # Result or output on success
"error": str # Error message on failure
}
```
**字段说明**
**Field Descriptions**:
| 字段 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `id` | `str` | 是 | 设备/节点/链接的唯一标识符 |
| `name` | `str` | 是 | 人类可读的名称 |
| `status` | `str` | 是 | `"success"` `"failed"` |
| `result` | `str` | 条件 | 状态为 `success` 时的输出 |
| `error` | `str` | 条件 | 状态为 `failed` 时的错误信息 |
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | `str` | Yes | Unique identifier for device/node/link |
| `name` | `str` | Yes | Human-readable name |
| `status` | `str` | Yes | `"success"` or `"failed"` |
| `result` | `str` | Conditional | Output when status is `success` |
| `error` | `str` | Conditional | Error message when status is `failed` |
## 示例
## Examples
### 成功响应示例
### Success Response Example
```python
# 执行多个设备的显示命令
# Execute display commands on multiple devices
{
"success": True,
"total": 3,
@ -96,10 +96,10 @@
}
```
### 完全失败示例
### Complete Failure Example
```python
# 整个操作失败(如参数错误)
# Entire operation failed (e.g., parameter error)
{
"success": False,
"total": 0,
@ -113,10 +113,10 @@
}
```
### 单个设备操作示例
### Single Device Operation Example
```python
# 操作单个设备
# Operate on a single device
{
"success": True,
"total": 1,
@ -134,56 +134,56 @@
}
```
## 使用标准化函数
## Using the Standardization Function
`gns3server.agent.gns3_copilot.utils` 模块中提供了 `normalize_tool_response` 函数,用于将各种格式转换为标准格式:
The `normalize_tool_response` function is provided in the `gns3server.agent.gns3_copilot.utils` module to convert various formats to the standard format:
```python
from gns3server.agent.gns3_copilot.utils import normalize_tool_response
# 标准化工具响应
# Normalize tool response
normalized = normalize_tool_response(raw_response, tool_name="my_tool")
```
该函数支持:
- 列表格式(`[{...}, {...}]`
- 字典格式(`{"nodes": [...]}`
- 字符串格式(自动解析 JSON/Python literal
- 混合格式(兼容旧工具)
This function supports:
- List format (`[{...}, {...}]`)
- Dict format (`{"nodes": [...]}`)
- String format (automatically parses JSON/Python literal)
- Mixed format (compatible with legacy tools)
## 兼容性
## Compatibility
### 向后兼容
### Backward Compatibility
`normalize_tool_response` 函数设计为向后兼容,可以处理现有工具的各种格式:
The `normalize_tool_response` function is designed to be backward compatible and can handle various formats from existing tools:
- `status` / `error` 字段
- `output` / `result` 字段
- `device_name` / `name` 字段
- `total_nodes` / `total` 字段
- `status` / `error` fields
- `output` / `result` fields
- `device_name` / `name` fields
- `total_nodes` / `total` fields
### 推荐的迁移策略
### Recommended Migration Strategy
1. **新工具**:直接返回标准格式
2. **现有工具**:保持不变,使用 `normalize_tool_response` 标准化
3. **前端**:依赖标准格式处理显示
1. **New Tools**: Return standard format directly
2. **Existing Tools**: Keep unchanged, use `normalize_tool_response` to standardize
3. **Frontend**: Rely on standard format for display processing
## 前端集成建议
## Frontend Integration Recommendations
### 渲染逻辑
### Rendering Logic
```javascript
function renderToolResponse(response) {
if (!response.success) {
// 显示全局错误
// Show global error
showError(response.error);
return;
}
// 显示统计摘要
// Show statistics summary
showSummary(response.total, response.successful, response.failed);
// 渲染每个项目
// Render each item
response.data.forEach(item => {
if (item.status === 'success') {
showSuccess(item.name, item.result);
@ -194,22 +194,22 @@ function renderToolResponse(response) {
}
```
### 状态图标
### Status Icons
| 状态 | 图标建议 | 颜色 |
|------|----------|------|
| `success` | ✓ 绿色 | 绿色 |
| `failed` | ✗ 红色 | 红色 |
| `unknown` | ? 灰色 | 灰色 |
| Status | Icon Suggestion | Color |
|--------|----------------|-------|
| `success` | ✓ Green | Green |
| `failed` | ✗ Red | Red |
| `unknown` | ? Gray | Gray |
## 版本控制
## Version Control
当前标准版本:`v1.0`
Current standard version: `v1.0`
格式变更时,应更新 `metadata.version` 字段,前端据此适配。
When the format changes, update the `metadata.version` field, and the frontend adapts accordingly.
## 参考
## References
- 实现:`gns3server/agent/gns3_copilot/utils/parse_tool_content.py`
- 消息转换:`gns3server/agent/gns3_copilot/utils/message_converters.py`
- 工具示例:`gns3server/agent/gns3_copilot/tools_v2/`
- Implementation: `gns3server/agent/gns3_copilot/utils/parse_tool_content.py`
- Message conversion: `gns3server/agent/gns3_copilot/utils/message_converters.py`
- Tool examples: `gns3server/agent/gns3_copilot/tools_v2/`

View File

@ -1,150 +1,150 @@
# Force Kill (kill -9) 导致残留进程问题
# Force Kill (kill -9) Causing Residual Processes Issue
## 问题描述
## Problem Description
当使用 `kill -9` 强制关闭 gns3server 进程后,重新启动 gns3server 会出现以下错误:
When using `kill -9` to forcibly close the gns3server process, restarting gns3server results in the following errors:
### 1. Dynamips VM 创建失败
### 1. Dynamips VM Creation Failure
```
ERROR gns3server.api.routes.compute:133 Compute node error: Dynamips error when running command 'vm create "R1" 1 c7200
': unable to create VM instance 'R1'
```
### 2. project_id 为 "undefined" 的验证错误
### 2. Validation Error with project_id as "undefined"
```
ERROR gns3server.api.server:208 Request validation error in /v3/projects/undefined/nodes/{node_id} (PUT):
1 validation error:
{'type': 'uuid_parsing', 'loc': ('path', 'project_id'), 'msg': 'Input should be a valid UUID, invalid character: expected an optional prefix of `urn:uuid:` followed by [0-9a-fA-F-], found `u` at 1', 'input': 'undefined', ...}
```
### 3. TCP 端口仍在使用的警告
### 3. TCP Port Still in Use Warning
```
WARNING gns3server.compute.project:355 Project d672144c-4de9-4a97-a23d-307ddc3ab9b1 has TCP ports still in use: {5001}
```
## 根本原因
## Root Cause
使用 `kill -9` 强制终止 gns3server 进序时gns3server 没有机会正确清理其启动的子进程,导致以下残留进程仍在运行:
When using `kill -9` to forcibly terminate the gns3server process, gns3server has no opportunity to properly clean up its spawned child processes, leaving the following residual processes running:
- **Dynamips hypervisor 进程** (dynamips)
- **VPCS 虚拟 PC 进程** (vpcs)
- **Docker 容器**(虽然在日志中显示被移除,但可能有些状态未清理)
- **其他模拟器进程**
- **Dynamips hypervisor processes** (dynamips)
- **VPCS virtual PC processes** (vpcs)
- **Docker containers** (although shown as removed in logs, some state may not be cleaned up)
- **Other emulator processes**
这些残留进程会:
1. 占用相同的端口号和资源 ID
2. 保持旧的 socket 连接
3. 导致新启动的 gns3server 无法重新分配相同资源
These residual processes will:
1. Occupy the same port numbers and resource IDs
2. Maintain old socket connections
3. Cause the newly started gns3server to be unable to reallocate the same resources
## 解决方案
## Solutions
### 方法 1手动清理残留进程推荐
### Method 1: Manual Cleanup of Residual Processes (Recommended)
在强制关闭 gns3server 后,查找并清理残留进程:
After forcibly closing gns3server, find and clean up residual processes:
```bash
# 查找 dynamips 进程
# Find dynamips processes
ps aux | grep dynamips
# 查找 vpcs 进程
# Find vpcs processes
ps aux | grep vpcs
# 终止残留进程
# Terminate residual processes
killall dynamips
killall vpcs
```
### 方法 2使用 pkill 清理相关进程
### Method 2: Use pkill to Clean Related Processes
```bash
# 清理所有 GNS3 相关进程
# Clean all GNS3 related processes
pkill -9 dynamips
pkill -9 vpcs
pkill -6 ubridge
pkill -9 ubridge
```
### 方法 3重启前检查
### Method 3: Check Before Restart
在重新启动 gns3server 之前,确保没有残留进程:
Before restarting gns3server, ensure there are no residual processes:
```bash
# 检查是否有残留的 GNS3 进程
# Check if there are residual GNS3 processes
ps aux | grep -E "(dynamips|vpcs|ubridge|gns3)" | grep -v grep
```
## 预防措施
## Preventive Measures
### 1. 使用正确的关闭方法
### 1. Use Proper Shutdown Methods
优先使用以下方法关闭 gns3server而不是 `kill -9`
Prefer the following methods to close gns3server instead of `kill -9`:
```bash
# 如果使用 systemd
# If using systemd
sudo systemctl stop gns3server
# 如果直接运行
# 按 Ctrl+C 或使用正常的 kill 信号
# If running directly
# Press Ctrl+C or use normal kill signal
kill <gns3server-pid>
```
### 2. 使用 SIGTERM 而不是 SIGKILL
### 2. Use SIGTERM Instead of SIGKILL
```bash
# 先尝试正常终止(允许进程清理)
# Try normal termination first (allows process to clean up)
kill -15 <gns3server-pid>
# 等待几秒,如果进程仍在运行,再使用 kill -9
# Wait a few seconds, if process is still running, then use kill -9
sleep 3
if ps -p <gns3server-pid> > /dev/null; then
kill -9 <gns3server-pid>
fi
```
### 3. 实现自动清理脚本
### 3. Implement Automatic Cleanup Script
可以创建一个启动脚本来检查并清理残留进程:
You can create a startup script to check and clean up residual processes:
```bash
#!/bin/bash
# cleanup_before_start.sh
# 检查并清理残留的 dynamips 进程
# Check and clean residual dynamips processes
if pgrep -f dynamips > /dev/null; then
echo "发现残留的 dynamips 进程,正在清理..."
echo "Found residual dynamips processes, cleaning up..."
killall -9 dynamips
fi
# 检查并清理残留的 vpcs 进程
# Check and clean residual vpcs processes
if pgrep -f vpcs > /dev/null; then
echo "发现残留的 vpcs 进程,正在清理..."
echo "Found residual vpcs processes, cleaning up..."
killall -9 vpcs
fi
# 等待端口释放
# Wait for ports to be released
sleep 1
# 启动 gns3server
# Start gns3server
gns3server
```
## 技术细节
## Technical Details
### 为什么 kill -9 会导致这个问题?
### Why Does kill -9 Cause This Problem?
1. **SIGKILL 信号无法被捕获**:进程无法捕获或忽略 SIGKILL 信号,因此没有机会执行清理代码
2. **子进程成为孤儿进程**:父进程被强制终止后,子进程被 init/PID 1 接管,但它们不知道父进程已死
3. **资源未释放**socket、端口、文件锁等资源没有被正确释放
4. **状态不一致**gns3server 的内部状态如端口分配、ID 分配)被清除,但实际资源仍被占用
1. **SIGKILL signal cannot be captured**: Processes cannot capture or ignore the SIGKILL signal, so there's no chance to execute cleanup code
2. **Child processes become orphans**: After the parent process is forcibly terminated, child processes are adopted by init/PID 1, but they don't know the parent has died
3. **Resources not released**: Sockets, ports, file locks, and other resources are not properly released
4. **State inconsistency**: gns3server's internal state (such as port allocation, ID allocation) is cleared, but actual resources are still occupied
### 涉及的代码位置
### Code Locations Involved
- **Dynamips 进程管理**`gns3server/compute/dynamips/`
- **端口分配和跟踪**`gns3server/compute/project.py:355`
- **节点更新 API**`gns3server/api/routes/controller/nodes.py:230`
- **Dynamips Process Management**: `gns3server/compute/dynamips/`
- **Port Allocation and Tracking**: `gns3server/compute/project.py:355`
- **Node Update API**: `gns3server/api/routes/controller/nodes.py:230`
## 相关问题
## Related Issues
- [ ] 考虑在 gns3server 启动时自动检测并清理残留进程
- [ ] 添加进程健康检查机制
- [ ] 实现更健壮的端口和 ID 重用逻辑
- [ ] 添加残留进程检测和警告
- [ ] Consider automatically detecting and cleaning up residual processes at gns3server startup
- [ ] Add process health check mechanism
- [ ] Implement more robust port and ID reuse logic
- [ ] Add residual process detection and warnings