Docker 容器化部署
- 提供 Mode A (Alpine musl, ~23MB) 和 Mode B (Distroless glibc, ~87MB)
两种镜像,Docker Compose 一键启动
- build.rs 支持 SKIP_DASHBOARD_BUILD 跳过前端构建
- 国内镜像加速 (npm/apt/apk) 通过 USE_MIRRORS build-arg 控制
安全:Cookie-Based 鉴权系统
- HttpOnly/SameSite=Strict Cookie 会话管理(24h 过期自动清理)
- 登录/登出/验证接口 + 中间件注入
- 前端登录页面 + 退出按钮
- 三层 CORS:localhost 鉴权 / 全放通 bookmarklet / 受保护路由
- 书签脚本 fetch 添加 credentials:'include'
Coordinator 模式 (P2)
- 4 个 meta-tool (delegate_task/check_task/task_stop/synthesize)
- WorkerPool + Semaphore 并发控制 + 超时保护
- 前端协调者模式开关
Hook 系统:UserPromptSubmit 事件 (P2)
- 第 13 个生命周期事件,fire-and-forget 审计
FTS5 全文搜索 (P3)
- agent_sessions_fts + agent_messages_fts 虚拟表
- search_history Agent 工具 + /api/search/history HTTP 接口
- 前端防抖搜索框 + 仅当前会话筛选
工具加载优化 (P3)
- defer_loading 延迟加载 (7 个重型工具)
- is_readonly 只读标记 (9 个查询工具)
- classifier_summary 工具目录供 LLM 按需判断
模型回退策略 (P3)
- LLM_FALLBACK_MODEL 优先回退 + LLM_FALLBACK_CHAIN 链式轮换
- LlmClient model 改为 Arc<RwLock> 支持运行时切换
- 连续 3 次过载后自动切换
压缩记忆桥接 (P3)
- 压缩丢弃消息 → 子代理提取持久记忆 (extract_memories_from_compaction)
git2 依赖修复
- 切换到 vendored-libgit2,消除 OpenSSL 系统依赖
414 lines
19 KiB
Markdown
414 lines
19 KiB
Markdown
# 子代理系统 (`subagent.rs` + `tools/subagent.rs`)
|
||
|
||
参考 Claude Code s04 Subagents 设计,实现**上下文隔离的子代理运行器**。父代理通过 `subagent` 工具将独立子任务委托给子代理执行,子代理拥有全新的消息上下文和完整的 ReAct 循环,仅将最终文本摘要返回给父代理。
|
||
|
||
核心价值:**防止父代理上下文被中间工具调用污染**,同时让子代理具备完整的工具能力(文献搜索、下载、RAG 检索、笔记等)来独立完成子任务。
|
||
|
||
## 架构总览
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph Tool["SubAgentTool (tools/subagent.rs)"]
|
||
direction TB
|
||
T_name["工具名: 'subagent' (对外语义: delegate_research)"]
|
||
T_params["参数: research_prompt (必需), max_steps (默认5, 最大10)"]
|
||
T_interrupt["InterruptBehavior::Block — 需等待写操作完成"]
|
||
T_role["职责: 参数解析 → 构造 SubAgentRunner → 结果包装"]
|
||
end
|
||
|
||
Tool -->|"调用"| Runner
|
||
|
||
subgraph Runner["SubAgentRunner (subagent.rs)"]
|
||
direction TB
|
||
R_fields["字段:<br/>• app_state: Arc<AppState><br/>• config: AgentConfig<br/>• tool_registry: ToolRegistry (与父代理共享)<br/>• hook_registry: Option<Arc<HookRegistry>><br/>• permission_checker: Arc<PermissionChecker><br/>• progress_tx: Option<UnboundedSender><br/>• parent_session_id: String"]
|
||
|
||
R_ctors["三种构造方式:<br/>① new() — 最简构造<br/>② new_with_hooks() — 完整构造<br/>③ new_with_registry() — 自定义工具集"]
|
||
|
||
R_chain["链式配置:<br/>• with_parent_session()<br/>• with_thinking()"]
|
||
end
|
||
```
|
||
|
||
## 完整执行流程
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Lead as 父代理 (AgentRuntime)
|
||
participant Tool as SubAgentTool
|
||
participant Runner as SubAgentRunner
|
||
participant LLM as LLM API
|
||
participant FE as 前端 SSE
|
||
participant DB as SQLite
|
||
|
||
Lead->>Tool: subagent(research_prompt, max_steps?)
|
||
Tool->>Tool: 参数校验 + 构造 system_prompt
|
||
Tool->>Runner: new_with_hooks(app_state, hooks, pchecker, sse_tx)
|
||
Tool->>Runner: .with_parent_session(sid).with_thinking(bool)
|
||
Tool->>Runner: run(system_prompt, research_prompt, max_steps)
|
||
|
||
Note over Runner: Phase 1 — 初始化
|
||
Runner->>Runner: 生成 subagent_name = "sub_<uuid8>"
|
||
Runner->>Runner: 触发 OnSubagentStart hook
|
||
Runner->>DB: 保存 system + user 消息 (fire-and-forget)
|
||
|
||
Note over Runner: Phase 2 — run_inner() ReAct 循环
|
||
Runner->>Runner: 构建全新 messages: [system, user]
|
||
|
||
loop 每步迭代 (step ≤ max_steps)
|
||
Runner->>Runner: 上下文压缩检查 (est_tokens > limit × 1.5)
|
||
Runner->>LLM: chat_stream(messages, tool_defs, thinking?)
|
||
LLM-->>Runner: ReasoningDelta / TextDelta / ToolCallsComplete
|
||
Runner->>FE: Thought "[子代理] ..." (通过 progress_tx)
|
||
|
||
alt 无工具调用 → 最终答案
|
||
Runner->>FE: Thought (子代理结论)
|
||
Runner-->>Tool: ToolOutput::success([子代理活动记录] + [子代理结论])
|
||
else 有工具调用
|
||
Runner->>Runner: 死循环检测 (连续相同调用 ≥ 3 次 → 拦截)
|
||
loop 每个 tool_call (串行执行)
|
||
Runner->>FE: ToolCall "[sub] tool_name" (通过 progress_tx)
|
||
Runner->>Runner: PreToolUse hook (Block → 跳过, MutateInput → 修改参数)
|
||
Runner->>Runner: 权限检查 (is_denied → 跳过)
|
||
Runner->>Runner: tool.execute(args, ctx) (timeout 保护)
|
||
Runner->>Runner: PostToolUse hook (MutateOutput → 修改输出)
|
||
Runner->>Runner: 输出截断 (max_tool_output_chars)
|
||
Runner->>FE: ToolResult "[sub] tool_name" (通过 progress_tx)
|
||
Runner->>DB: 保存 assistant + tool 消息 (fire-and-forget)
|
||
Runner->>Runner: messages.push(ChatMessage::tool_result)
|
||
end
|
||
Runner->>Runner: messages.push(ChatMessage::assistant)
|
||
end
|
||
end
|
||
|
||
Note over Runner: Phase 3 — 达到最大步数
|
||
Runner->>LLM: force_final_answer (空工具列表, 无工具调用)
|
||
LLM-->>Runner: 最终文本答案
|
||
|
||
Note over Runner: Phase 4 — 收尾
|
||
Runner->>DB: 保存最终 assistant 消息
|
||
Runner->>Runner: 触发 OnSubagentStop hook
|
||
Runner-->>Tool: ToolOutput { content, metadata }
|
||
Tool->>Tool: 包装: "[子代理研究结果]\n\n{content}"
|
||
Tool-->>Lead: ToolOutput
|
||
```
|
||
|
||
## 核心机制详解
|
||
|
||
### 1. 上下文隔离
|
||
|
||
子代理拥有**全新的 `messages` 向量**,仅包含 system prompt + user prompt,不包含父代理的任何中间工具调用。父代理只会收到最终的文本摘要,中间的工具调用细节(搜索了什么、下载了什么)不会污染父上下文。
|
||
|
||
这是子代理系统最核心的价值——与直接在父代理中执行相比,子代理消耗的父上下文 token 是 O(1) 而不是 O(steps)。
|
||
|
||
```
|
||
父代理 context: 子代理 context:
|
||
┌─────────────────────┐ ┌─────────────────────────┐
|
||
│ system prompt │ │ system prompt │
|
||
│ user: "分析星系演化" │ │ user: "搜索星系演化的文献" │
|
||
│ assistant: tool_calls│ │ assistant: tool_call │
|
||
│ tool_result: [大文本] │ │ tool_result: [搜索结果] │
|
||
│ ... (越来越多) │ │ assistant: "找到 5 篇..." │
|
||
│ │ └─────────────────────────┘
|
||
│ ← 子代理结果注入这里 │ ↓
|
||
│ (仅有摘要,无中间步骤)│ 只返回文本摘要给父代理
|
||
└─────────────────────┘
|
||
```
|
||
|
||
### 2. 工具执行模式 — 串行
|
||
|
||
与父代理 `executor.rs` 使用 `FuturesUnordered` 的**并行**执行不同,子代理采用简单的 **for 循环串行**执行:
|
||
|
||
```rust
|
||
// src/agent/subagent.rs:395 — 串行 for 循环
|
||
for tool_call in &tool_calls {
|
||
let output = tool.execute(final_args, &tool_ctx).await;
|
||
messages.push(ChatMessage::tool_result(&tool_call.id, &truncated));
|
||
}
|
||
```
|
||
|
||
| 特性 | 父代理 (executor.rs) | 子代理 (subagent.rs) |
|
||
|:---|:---|:---|
|
||
| 执行方式 | `FuturesUnordered` 并行 | `for` 循环串行 |
|
||
| 结果推送 | 渐进式(快工具不等待慢工具) | 逐个完成 |
|
||
| FileStateCache | 支持(避免重复读文件) | 不支持 |
|
||
| 大结果持久化到磁盘 | `maybe_persist_tool_result` | 无(仅截断) |
|
||
| Sibling Abort | 支持(出错时中断兄弟任务) | 不支持 |
|
||
|
||
**设计考量**:子代理的 `max_steps` 通常为 5,任务规模较小,并行收益有限。串行实现更简单、更可预测,且每次工具调用之前会检查 PreToolUse hook 和权限,串行执行保证了 hook 决策的时序正确性。
|
||
|
||
### 3. 死循环检测
|
||
|
||
子代理独立维护 `last_call: Option<(String, String)>` 追踪上次的工具调用(名称 + 参数)。当连续 3 次相同调用时触发拦截:
|
||
|
||
```rust
|
||
// src/agent/subagent.rs:416-418
|
||
let call_key = (tool_name.clone(), tool_args_str.clone());
|
||
if last_call.as_ref() == Some(&call_key) {
|
||
consecutive_count += 1;
|
||
if consecutive_count >= duplicate_threshold { // threshold = 3
|
||
// 注入错误消息,强制 LLM 停止并给出答案
|
||
let error_msg = ChatMessage::tool_result(
|
||
&tool_call.id,
|
||
format!("工具 {} 被连续重复调用。请停止并给出当前收集到的答案。", tool_name),
|
||
);
|
||
messages.push(error_msg);
|
||
continue; // 跳过本次执行
|
||
}
|
||
} else {
|
||
last_call = Some(call_key);
|
||
consecutive_count = 1;
|
||
}
|
||
```
|
||
|
||
这与父代理的 `DuplicateDetector` 功能相同,但实现更简洁——子代理的场景简单,不需要全局去重表。
|
||
|
||
### 4. 上下文压缩
|
||
|
||
子代理的上下文在每步开始前检查,使用简单的字节长度估算:
|
||
|
||
```rust
|
||
// src/agent/subagent.rs:261-265
|
||
let est_tokens: usize = messages
|
||
.iter()
|
||
.map(|m| m.content.as_ref().map_or(0, |c| c.len()) + 4)
|
||
.sum();
|
||
if est_tokens > self.config.context_char_limit * 3 / 2 {
|
||
compact::compress_context(&mut messages, llm, config.context_char_limit, "subagent").await;
|
||
}
|
||
```
|
||
|
||
- 触发阈值:`context_char_limit * 1.5`(默认 ~24000 字符)
|
||
- 压缩方式:调用 `compact::compress_context`(LLM 摘要压缩)
|
||
- 标识来源:`"subagent"`,区别于 `"lead"` / `"teammate"` / `"background"`
|
||
|
||
### 5. force_final_answer — 达到最大步数时的兜底
|
||
|
||
```rust
|
||
// src/agent/subagent.rs:563-600
|
||
async fn force_final_answer(&self, llm: &LlmClient, messages: &[ChatMessage]) -> ToolOutput {
|
||
let mut final_messages = messages.to_vec();
|
||
final_messages.push(ChatMessage::user(
|
||
"请根据已收集的信息直接给出最终答案,不要再调用工具。",
|
||
));
|
||
let empty_tools: Vec<ToolDefinition> = Vec::new(); // 不提供任何工具
|
||
let mut stream_rx = llm.chat_stream(&final_messages, &empty_tools, ...).await?;
|
||
// 收集 TextDelta 直到 Done
|
||
}
|
||
```
|
||
|
||
- 追加一条 user 消息明确要求停止工具调用
|
||
- 传递**空的工具定义列表**,从 API 层面禁止工具调用
|
||
- 返回 `metadata: { "forced": true }` 标记这是强制答案
|
||
|
||
### 6. 数据库持久化
|
||
|
||
所有子代理消息通过 `save_subagent_message` 写入 `agent_messages` 表,使用 fire-and-forget 模式(`tokio::spawn`):
|
||
|
||
```rust
|
||
// src/agent/subagent.rs:190-235
|
||
fn save_subagent_message(&self, agent_name: &str, turn_index: i32, step_index: i32,
|
||
role: &str, content: &str, ...) {
|
||
let metadata = serde_json::json!({ "agent": agent_name, "is_subagent": true });
|
||
tokio::spawn(async move {
|
||
let _ = sqlx::query(
|
||
"INSERT INTO agent_messages (session_id, turn_index, step_index, role,
|
||
content, thought, tool_calls, tool_call_id, token_count, metadata, agent_name)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||
)
|
||
.bind(&session_id)
|
||
.bind(turn_index) // 始终为 0
|
||
.bind(step_index) // 当前 step 编号
|
||
.bind(&role_owned) // system / user / assistant / tool
|
||
.bind(&content_owned)
|
||
// ...
|
||
.bind(&metadata_str) // {"agent":"sub_xxx","is_subagent":true}
|
||
.bind(&agent) // subagent_name
|
||
.execute(&db).await;
|
||
});
|
||
}
|
||
```
|
||
|
||
持久化的消息类型:
|
||
- **system** — 子代理系统提示词
|
||
- **user** — 研究任务描述
|
||
- **assistant** — 每步 LLM 响应(含 tool_calls JSON)
|
||
- **tool** — 每个工具的执行结果(截断后)
|
||
- **assistant** — 最终结果(step_index = max_steps + 1)
|
||
|
||
所有消息带 `{"is_subagent": true}` 元数据标记,`agent_name` 字段存放 `sub_<uuid8>` 名称,`turn_index` 始终为 0。
|
||
|
||
**设计考量**:fire-and-forget 意味着持久化失败不会阻塞子代理执行。代价是极端情况下(DB 连接断开)可能丢失审计数据。
|
||
|
||
## Hook 集成
|
||
|
||
子代理在以下时机触发 Hook 事件:
|
||
|
||
| 事件 | 触发位置 | 上下文数据 | 用途 |
|
||
|:---|:---|:---|:---|
|
||
| `OnSubagentStart` | `run()` Phase 1 | parent_session_id, subagent_name, prompt | 审计、指标初始化 |
|
||
| `PreToolUse` | 每个工具执行前 | session_id="subagent", tool_name, args | 取消检查、参数修改 |
|
||
| `PostToolUse` | 每个工具执行后 | session_id="subagent", output_content, is_error | 指标采集、输出修改 |
|
||
| `OnSubagentStop` | `run()` Phase 4 | result_summary (前200字符), steps, is_error | 审计记录、数据汇总 |
|
||
|
||
`PreToolUse` 和 `PostToolUse` 在子代理的工具执行循环中**直接内联实现**(`subagent.rs:444-521`),而非通过 `executor.rs` 的并行管道。这意味着:
|
||
|
||
- PreToolUse 的 Block 操作 → 注入错误 tool_result 消息,跳过执行
|
||
- PreToolUse 的 MutateInput → 替换 final_args
|
||
- PostToolUse 的 MutateOutput → 替换 final_output_content
|
||
- Permission check → `is_denied` 直接拒绝
|
||
|
||
**与父代理的区别**:父代理通过 `executor::execute_parallel()` 统一处理 hooks + 并发,子代理在自身的 for 循环中手动调用 hook 方法。两者享有相同的 `HookRegistry` 实例(通过 `SubAgentRunner::new_with_hooks` 注入)。
|
||
|
||
## SubAgentTool 工具层
|
||
|
||
```rust
|
||
// src/agent/tools/subagent.rs
|
||
impl AgentTool for SubAgentTool {
|
||
fn name(&self) -> &str { "subagent" }
|
||
|
||
fn parameters(&self) -> serde_json::Value {
|
||
json!({
|
||
"type": "object",
|
||
"properties": {
|
||
"research_prompt": {
|
||
"type": "string",
|
||
"description": "要委托给子代理执行的完整研究任务描述..."
|
||
},
|
||
"max_steps": {
|
||
"type": "integer",
|
||
"description": "子代理最大推理步数,默认5,最大10",
|
||
"default": 5
|
||
}
|
||
},
|
||
"required": ["research_prompt"]
|
||
})
|
||
}
|
||
|
||
// 子代理可能执行写操作,中断时应阻塞以完成
|
||
fn interrupt_behavior(&self) -> InterruptBehavior {
|
||
InterruptBehavior::Block
|
||
}
|
||
}
|
||
```
|
||
|
||
`execute` 方法流程:
|
||
1. 参数校验(`research_prompt` 必填,`max_steps` 取 min(参数, 10))
|
||
2. 构造子代理 `system_prompt`(天体物理学研究助手,中文回答)
|
||
3. 创建 `SubAgentRunner::new_with_hooks()` — 注入 hooks、permissions、SSE 通道、session_id
|
||
4. 调用 `runner.run(system_prompt, &research_prompt, max_steps)`
|
||
5. 错误 → `ToolOutput::error("子代理执行失败: ...")`
|
||
6. 成功 → 包装为 `"[子代理研究结果]\n\n{content}"`
|
||
|
||
## 与 Team Teammate 的对比
|
||
|
||
两者都实现了"将工作委托给独立的 ReAct 循环",但设计上有本质差异:
|
||
|
||
| 特性 | SubAgentRunner | Team Teammate |
|
||
|:---|:---|:---|
|
||
| 文件位置 | `src/agent/subagent.rs` | `src/agent/team/teammate.rs` |
|
||
| 触发方式 | LLM 调用 `subagent` 工具 | Lead 通过文件收件箱发送任务 |
|
||
| 工具集 | 完整 ToolRegistry(含 subagent) | 排除 subagent(防无限委托链) |
|
||
| Hook 管道 | 完整(PreToolUse/PostToolUse/Start/Stop) | 无 |
|
||
| SSE 进度 | 支持(通过 progress_tx 透传) | 无 |
|
||
| DB 持久化 | 完整(agent_messages + agent_audit_log) | 无 |
|
||
| 运行模式 | 同步:父代理等待子代理完成 | 异步:循环轮询收件箱 |
|
||
| 生命周期 | 一次性:任务完成即销毁 | 持续:SPAWN → WORKING → IDLE → SHUTDOWN |
|
||
| 嵌套能力 | 可递归(无深度限制) | 不可(排除 subagent 工具) |
|
||
| 上下文压缩 | 支持(compact::compress_context) | 支持(同) |
|
||
| 死循环检测 | 支持(阈值=3) | 无独立检测 |
|
||
| 最大步数 | 默认 5,最大 10 | `max_steps.min(5)` |
|
||
| 取消支持 | 无独立取消信号 | `AtomicBool` 取消标志 |
|
||
|
||
**使用场景区分**:
|
||
- **SubAgent** — 同步的、一次性的、需要完整工具能力的子任务("综述近 5 年星系演化的文献")
|
||
- **Teammate** — 异步的、持续性的、角色分工的并行协作("你负责搜索,他负责解析")
|
||
|
||
## 工具注册
|
||
|
||
子代理工具 `SubAgentTool` 在 `ToolRegistry::add_base_tools()` 中注册,随后被替换为带 hooks 的实例:
|
||
|
||
```rust
|
||
// src/agent/runtime/mod.rs:264
|
||
// AgentRuntime 初始化时用带 hooks 的版本替换默认的 SubAgentTool
|
||
tool_registry.replace_tool(
|
||
crate::agent::tools::subagent::SubAgentTool::new_with_hooks(
|
||
hook_registry.clone(),
|
||
permission_checker.clone(),
|
||
),
|
||
);
|
||
```
|
||
|
||
参数透传链路:
|
||
```
|
||
LLM 调用 subagent(research_prompt, max_steps)
|
||
→ SubAgentTool.execute(args, ToolContext)
|
||
→ SubAgentRunner::new_with_hooks(app_state, hooks, pchecker, ctx.sse_tx)
|
||
.with_parent_session(ctx.session_id)
|
||
.with_thinking(ctx.enable_thinking)
|
||
.run(system_prompt, research_prompt, max_steps)
|
||
```
|
||
|
||
## 外部集成场景
|
||
|
||
`SubAgentRunner` 除了通过 `SubAgentTool` 被父代理调用外,还被以下两个子系统使用:
|
||
|
||
### 1. Coordinator WorkerPool(P2)
|
||
|
||
协调者模式中,`WorkerPool::delegate()` 为每个委托任务创建一个 `SubAgentRunner` 实例:
|
||
|
||
```
|
||
CoordinatorAgent
|
||
→ delegate_task(task_description, context)
|
||
→ WorkerPool::delegate()
|
||
→ tokio::spawn(Worker)
|
||
→ SubAgentRunner::new_with_registry(app_state, 完整 ToolRegistry)
|
||
.with_parent_session(session_id)
|
||
.with_thinking(enable_thinking)
|
||
.run(WORKER_SYSTEM_PROMPT, task_description, worker_max_steps)
|
||
→ 结果写入 CoordinatorTask.result
|
||
→ synthesize()
|
||
→ 收集所有已完成 Worker 结果 → 合成最终答案
|
||
```
|
||
|
||
关键特征:
|
||
- Worker 子代理拥有**完整工具访问权限**(与 Coordinator 的 4 个元工具相对)
|
||
- 通过 `Semaphore` 控制最大并发(默认 4)
|
||
- 超时保护(默认 300s),超时后标记为 `TimedOut`
|
||
- 结果通过共享 `CoordinatorTask` 结构返回,不经过 SSE
|
||
|
||
### 2. 压缩记忆提取(P3 桥接)
|
||
|
||
`compact::extract_memories_from_compaction()` 创建仅含 `SaveMemoryTool` 的受限子代理:
|
||
|
||
```
|
||
AgentRuntime::compress_and_restore()
|
||
→ compress_context_with_hooks_and_log(...)
|
||
→ extract_memories_from_compaction(snapshot, sid, mgr, app_state)
|
||
→ SubAgentRunner::new_with_registry(app_state, SaveMemoryTool 仅此一个)
|
||
.with_parent_session(sid)
|
||
.with_thinking(false)
|
||
.run(EXTRACTION_SYSTEM_PROMPT, prompt, max_steps=3)
|
||
→ 写入记忆(fire-and-forget,不阻塞 ReAct 循环)
|
||
```
|
||
|
||
关键特征:
|
||
- 工具集最小化(仅 `save_memory`)——只做记忆写入,不能读文件或搜索
|
||
- 思考模式关闭(`with_thinking(false)`)——降低提取成本
|
||
- 内容阈值保护:拼接文本 < 300 字符时跳过
|
||
- 与 `run_extraction()`(会话结束时触发)互为补充——详见 [memory.md](memory.md)
|
||
|
||
## 当前局限与改进方向
|
||
|
||
| # | 问题 | 影响 | 改进方向 |
|
||
|:---|:---|:---|:---|
|
||
| 1 | **工具串行执行** | 同一步多个工具调用无法并行,慢工具阻塞快工具 | ~~复用 executor~~ 已通过 Coordinator WorkerPool 在多 Worker 层面实现并行;单 Worker 内仍串行 |
|
||
| 2 | **无嵌套深度限制** | 子代理可调用 subagent 创建子子代理,理论上无限递归 | 增加深度计数器,超过 2 层时移除 subagent 工具 |
|
||
| 3 | **Token 估算粗糙** | `content.len() + 4` 对中文极不准确(中文 1 字符 ≈ 1.5-2 token) | 使用 `tiktoken-rs` 或 tokenizer 精确计数 |
|
||
| 4 | **fire-and-forget 持久化** | DB 写入失败静默忽略,可能丢失审计数据 | 至少记录 warn 日志;关键消息可改为 await |
|
||
| 5 | **force_final_answer 不调温度** | 达到 max_steps 时模型可能仍尝试输出工具调用格式 | 降低 temperature 或增加 stop 序列 |
|
||
| 6 | **subagent_name 不可控** | 自动生成的 UUID 片段不利于日志可读性 | 允许 LLM 传入 `name` 参数作为标识 |
|
||
| 7 | **无 FileStateCache** | 子代理反复读取同一文件时会重复 I/O | 传入 FileStateCache 或使用全局缓存 |
|
||
| 8 | **步骤内消息顺序丢失** | 同一步的多个 tool 消息共享相同 step_index | 引入子序号(如 step_index.sub_index) |
|
||
|
||
---
|
||
|