Files
homeagent-sdk/docs/guide/parallel-tool-declaration.md
JianFeeeee 82e8d9dbac docs: 补两篇指南 —— 工具并发声明、流式多 tool_call
`ParallelSafe` / `Serial` / `stream_index` 三个新能力此前**零文档**:
README 提到 `Serial` 的那处是 UART 串口,与并发声明无关。插件作者只能
读源码注释才能知道这些字段存在及其优先级。

## docs/guide/parallel-tool-declaration.md

- 保守 opt-in 的理由:存量插件不改一行就得串行,不会被升级意外并发
- 声明 `ParallelSafe` 的三个条件(线程安全 / 不争抢资源 / 顺序无关)
- `Serial` 存在的意义:让"我确认过**必须**串行"与"我没想过"可区分
- **`Serial` 胜出**,不允许被 `ParallelSafe` 或默认值覆盖
- 声明字段放在 `ToolDef` 末尾,遵循既有 `NoMemory` 风格
- 压测效果 N=2/4/8 → 1.41×/2.22×/3.88×,并附"必须同时统计实际执行数"
  的理由(旧适配器耗时更短但实际处理 0 个工具)

## docs/guide/stream-tool-call-index.md

面向写 Lua 适配器的人:

- 上游 `index` 字段的用途:分桶累积 `id`/`name`/`arguments`
- **键名是 `stream_index` 不是 `index`** —— 写错会被 Go 解码器静默丢弃
- 不透传的实际后果:name 互相覆盖、args 碎片混拼、工具被当空参数调用
- 顺带记两个易踩点:不能按 name 过滤分片;扁平结构的协议族同样要带
- 自检命令;并注明 `gemini.lua` 不涉及(协议是 `functionCall`)

## 其它

- `mkdocs.yml` nav 登记两篇 —— 之前它们会被 mkdocs 明确警告
  "not included in the nav configuration",等于在站点里不可达
- 重跑 `tools/apidoc/build.sh` 同步 `docs/api/*`、`llms.txt`(生成物)

核实过的事实,避免臆造:
- 内置工具声明是**核心仓**的 `toolDefOptions`/`parallelOpts()`
  (`internal/agent/core/tooldefs.go`),不是 `sdk.BuiltinToolDef` —— 初稿写错
- `gemini.lua` 对 `functionCall|tool_calls` 匹配数为 0,确认无流式实现
- `server/kimicode/anthropic/ollama` 四个适配器确有 `stream_index`(2~5 处)
- 跨仓相对链接不可解析,故改为纯文本路径指路

`docs/api/tools.md` 属生成物(首行注明"请勿手改"),其 `ToolDef` 签名被截断、
不展示字段 —— 这是生成器既有行为,本次 diff 只是行号漂移,未改它。
2026-09-27 22:25:38 +08:00

113 lines
4.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 工具并发声明:`ParallelSafe` / `Serial`
> 对应 `sdk.ToolDef` 的两个字段。内核在**同一轮**收到多个 `tool_call` 时,
> 依据它们决定并发还是整批串行。
>
> 状态:已随 2026-09 的并行内核落地并在生产启用。
## 1. 为什么是"保守 opt-in"
**默认整批串行。** 只有当**批内每一个**工具都显式声明 `ParallelSafe: true`
时,那一批才并发;**只要有一个不声明,整批退回串行**。
这不是"漏了声明导致退化"的将就,而是刻意的设计:
- 存量插件**不改一行**就得到保守行为(整批串行),不会被升级意外并发
- 声明是**责任**而非特权 —— 声明者必须自己确认线程安全
- 宁可慢,不可错:一次错误的并发可能让两个工具抢同一个 SQLite 写、
同一台设备、或同一个输出通道
```go
// 同批全是安全工具 → 并发
tools: [a(ParallelSafe), b(ParallelSafe)] ⇒ 并发
// 只要有一个没声明 → 整批串行
tools: [a(ParallelSafe), b(默认)] ⇒ 串行
```
## 2. 三个条件都满足才可以声明 `ParallelSafe`
1. **handler 自身线程安全** —— 不持有跨调用的可变状态
2. **不与同批其它工具争抢同一资源** —— SQLite 写、设备、同一输出通道
3. **执行顺序无关** —— 顺序敏感的工具应留 `false`,由内核保序
第 3 条常被忽略:内核能保证**用户可见的消息**按声明顺序落盘,但**工具
之间的实际执行先后**在并发模式下不确定。有顺序依赖就留 `false`。
## 3. `Serial`:显式的反向标记
```go
Serial bool `json:"serial,omitempty"`
```
`ParallelSafe` 的零值 `false` 已经表达"串行",插件**无法区分**:
- "我没想过"
- "我确认过**必须**串行,且有原因"
一旦工具作者需要把"这里**故意**串行,是有原因的"写进代码(而不只是没填),
这个区分就是必需的 —— 否则只能靠命名约定传递意图。
适用场景:读操作但有隐含顺序约束(终端 `read`/`resize` 这类共享会话
状态);写操作虽已加锁但需要串行以获得可预测的交错顺序。
**优先级:`Serial` 胜出。** 即使同时写了 `ParallelSafe: true`,`Serial`
仍然生效 —— 显式声明"必须串行"不允许被 `ParallelSafe` 或任何默认值覆盖。
## 4. 写法
声明字段放在 `ToolDef` 结构体的**末尾**,遵循既有 `NoMemory` 的风格:
```go
sdk.RegisterTool(sdk.ToolDef{
Name: "my_readonly_query",
Description: "……",
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
Handler: h.query,
ParallelSafe: true, // 声明在末尾
}, s)
```
需要"故意串行"时:
```go
sdk.RegisterTool(sdk.ToolDef{
Name: "my_terminal_input",
Handler: h.input,
Serial: true, // 胜出,忽略 ParallelSafe
}, s)
```
## 5. 内置工具
核心仓的内置工具用 `toolDefOptions` / `parallelOpts()` 声明
(`internal/agent/core/tooldefs.go`),最终由 `buildToolDefs` 汇总成
`ToolDef.ParallelSafe`。
内核只提供并行调度基础设施,**不硬编码任何工具名的安全状态表** ——
状态由每个工具在自己的声明结构里给出,查询时走聚合表。
## 6. 效果(实测)
同批 N 个各约 250ms 的工具,新版内核"内部并发"对"强制串行":
| N | 加速比 |
| --- | --- |
| 2 | ~1.41× |
| 4 | ~2.22× |
| 8 | ~3.88× |
批耗时几乎不随 N 增长,串行批严格线性。
> 压测时**必须同时记录实际执行的工具数**,不能只看耗时。
> 某次对照中旧版本耗时更短、但因适配器缺 `stream_index` 导致
> **实际处理 0 个工具** —— 那不是性能提升,是全失败。
## 相关
设计背景与踩坑见**核心仓**文档(不在本仓):
- `docs/zh/toolcall-contract-and-sequence-design.md` —— 契约与序列设计
- `docs/zh/toolcall-parallel-execution-plan.md` —— 阶段、实测压测数据
- `docs/zh/deploy-runbook.md` —— 生产部署(含适配器 `stream_index` 相关)