135 lines
8.8 KiB
Markdown
135 lines
8.8 KiB
Markdown
|
|
# 智能兜底(Pi Agent)
|
|||
|
|
|
|||
|
|
> 对齐《Pi-Agent兜底能力详细方案》§4/§6 与 `poc/pi-fallback/`(GOAL-P1 / P1-DESIGN / P1-IMPL-NOTES / P1-VALIDATION)。
|
|||
|
|
> 落地态:**P1 只读兜底**(方案 S1 意图未识别 / S5 自由分析进产品路径)。
|
|||
|
|
|
|||
|
|
## 定位
|
|||
|
|
|
|||
|
|
用户在对话里说了一句产品功能覆盖不了的话(意图识别落到 `assistant.reply`/`unknown` 分支)时,
|
|||
|
|
不再只得到固定话术,而是拉起 Pi headless 做**一次只读分析**,返回一份真实产出的分析草稿:
|
|||
|
|
|
|||
|
|
- **草稿语义(P1)**:产物只是回复文本与 run 目录归档,**不写主干世界**;
|
|||
|
|
回复带 `[智能兜底 · 草稿]` 前缀与「未改动任何数据」声明。
|
|||
|
|
- **失败显式**:任何失败(熔断/不可用/伪造凭证/空报告)都回退原话术 + 一行中文化失败原因,
|
|||
|
|
并写 FAILED 审计;`propose_reply` 绝不抛出,聊天链路永远有回复。
|
|||
|
|
- **接线点唯一**:`server/aps_domain/workflow.py` 的 `assistant.reply/unknown` 分支
|
|||
|
|
→ `fallback_lane.propose_reply()`;开关关时返回 `None`,原路径逐字节不变。
|
|||
|
|
|
|||
|
|
## 组件
|
|||
|
|
|
|||
|
|
| 组件 | moduleId | 职责 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| `server/agent_core/fallback_lane.py` | core-fallback-lane | 兜底编排器:触发判定、任务简报、拉起/回收 Pi 子进程、三重熔断、环境清洗、run 目录管理、审计 |
|
|||
|
|
| `server/integrations/pi_bridge.py` | integ-pi-bridge | 围墙内工具桥:只读工具注册表、callId 凭证账(calls.jsonl)、报告凭证校验、只读快照导出、任务简报模板 |
|
|||
|
|
|
|||
|
|
Harness 登记:`agent.fallback.propose = P1`(见 [harness.md](./harness.md) 权力矩阵)。
|
|||
|
|
它只是权力登记动作名,**不是意图枚举成员**(`IntentName` 封闭 Literal 未变),LLM 无法产出它。
|
|||
|
|
|
|||
|
|
## 开关语义(FF-01 `fallback`,默认关)
|
|||
|
|
|
|||
|
|
- 配置:复用 FF-01 文件化开关(`server/data/features.json`,`APS_FEATURES_PATH` 可覆盖);
|
|||
|
|
`{"features": {"fallback": true}}` 显式开启。
|
|||
|
|
- **默认关**:`feature_flags._DEFAULT_OFF_KEYS = {"fallback"}`——文件缺失/损坏/未配置/取值非 bool
|
|||
|
|
时 `fallback` 默认 **False**,与其余键的 fail-open **相反**;`/api/features` 响应携带
|
|||
|
|
`defaultOff` 字段显式标注。
|
|||
|
|
- **为什么相反**:fallback 会拉起外部 LLM 子进程并产生 token 成本,默认开会在无模型/离线现场
|
|||
|
|
制造意外副作用;兜底是增强能力而非主干功能,关了只回到现状话术,系统不失能(有意取舍,
|
|||
|
|
fail-open 的「损坏不锁死」保护对该键反向成 fail-closed)。
|
|||
|
|
- 查询入口 `fallback_feature_enabled()` 直接消费 `load_feature_flags()` 单一事实源;
|
|||
|
|
任何异常 → False(宁可误关不可误开)。
|
|||
|
|
|
|||
|
|
## 运行目录
|
|||
|
|
|
|||
|
|
根:`<APS_FALLBACK_DIR>` 或 `path_under_data("fallback")/`
|
|||
|
|
(web=`server/data/fallback/`,desktop=`~/.aps/data/fallback/`)。
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
<根>/
|
|||
|
|
├── pi-home/models.json # PI_CODING_AGENT_DIR 配置圈禁;apiKey 只写环境变量名引用,不落明文
|
|||
|
|
└── fb-YYYYMMDD-HHMMSS-xxxxxx/
|
|||
|
|
├── inbox/ # 注入区:snapshot.md / orders.csv(export_snapshot 只读快照)
|
|||
|
|
├── work/ # pi 子进程 cwd 圈禁于此
|
|||
|
|
├── outbox/report.md # 产物唯一出口
|
|||
|
|
├── events.jsonl / orchestrator.log / result.json / calls.jsonl
|
|||
|
|
└── guard-<runId>.ts # 本次运行加载的守卫扩展(随运行归档)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
配置环境变量(`APS_FALLBACK_*`):`DIR` / `PI_CLI`(默认复用 `poc/pi-fallback/runtime/` 的 P0 安装,
|
|||
|
|
打包留后续阶段)/ `PI_HOME` / `NODE` / `MODEL` / `TIMEOUT_SEC`(90) / `MAX_STEPS`(30) /
|
|||
|
|
`MAX_OUTPUT_BYTES`(2MiB)。模型 key 复用 `LLM_BASE_URL`/`LLM_API_KEY`/`LLM_MODEL`,只经白名单清洗后的
|
|||
|
|
子进程环境注入,**绝不打印、绝不落盘**;`resolve_model()` 经 `GET /models` 协商(避开 P0 实测的
|
|||
|
|
404 坑),进程内缓存 300s。
|
|||
|
|
|
|||
|
|
node 解析顺序(`_resolve_node`,P1 真实冒烟坑 B 对策):① 显式 `APS_FALLBACK_NODE` 最高优先级
|
|||
|
|
原样命中;② 默认 `"node"` 且 Windows 时 `shutil.which("node.exe")` **优先于** `shutil.which("node")`
|
|||
|
|
——避开 PATH 中先于 node.exe 命中的 `node.CMD` 垫片(Popen 起 .cmd 引号语义会炸,pi 秒败,
|
|||
|
|
2026-09-02 冒烟实测);③ 其余平台/兜底回退 `shutil.which("node")`。
|
|||
|
|
|
|||
|
|
## 三重熔断与成败判定
|
|||
|
|
|
|||
|
|
| 闸 | 上限 | 触发后果 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| 超时 | 90s(聊天同步预算;executor 线程内运行,不堵事件循环) | 杀进程树,`breaker:timeout(...)` 显式判败 |
|
|||
|
|
| 工具步数 | 30 | 同上,`breaker:max_steps(...)` |
|
|||
|
|
| 输出体量 | 2MiB | 同上,`breaker:max_output(...)` |
|
|||
|
|
|
|||
|
|
成败**只看事件流 stopReason**(`stop` 才为成功),绝不相信进程退出码(P0 实测 pi 恒退 0);
|
|||
|
|
`stop` 但报告为空判 `error:empty_report`;runner 抛错/桥违规归并 `harness_error`;
|
|||
|
|
运行时不可用(无 node/pi/模型配置)判 `unavailable:<原因>`。
|
|||
|
|
|
|||
|
|
## 围墙与工具桥只读面
|
|||
|
|
|
|||
|
|
- **L1 工具层**:pi 启动参数只挂 `read,grep,find,ls` 只读四件套;守卫扩展在 pi 侧拦截
|
|||
|
|
`bash/edit/write` 全禁、文件工具路径限 run 目录(拦截落 guard-blocked-calls.jsonl)。
|
|||
|
|
- **L2 目录圈禁**:子进程 cwd=work/;桥侧 `handle_fs_read` resolve + is_relative_to,
|
|||
|
|
越界(`../`、绝对路径逃逸)抛 `ToolBridgeViolation`。
|
|||
|
|
- **L4 环境清洗**:白名单制子进程环境,剥离 `CONDA_*`/`PYTHON*`/`PIP_*`/`VIRTUAL_ENV*`。
|
|||
|
|
|
|||
|
|
桥注册表(`TOOL_REGISTRY`)P1 只暴露三项,写类工具(fs_write/shell_run/aps_invoke 等)
|
|||
|
|
**刻意不登记——不登记即不可见,这是墙的一部分**:
|
|||
|
|
|
|||
|
|
| 工具 | 权力 | 语义 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| `fs_read` | P0 | 读 run 目录内文件(真实 pi 侧由内置 read/grep/find/ls + 守卫扩展实现) |
|
|||
|
|
| `aps_query` | P0 | 快照制:run 启动时把只读世界视图导出为 inbox/snapshot.md + orders.csv,pi 经 fs_read 消费;**不做 pi 进程内实时查询工具** |
|
|||
|
|
| `report_emit` | P1 | 产物唯一出口:编排器把 pi 最终文本写 outbox/report.md |
|
|||
|
|
|
|||
|
|
**callId 凭证**:每次工具事件由编排器在围墙外签发 `call-<uuid4>` 落 calls.jsonl
|
|||
|
|
(入参只落 sha256 摘要,不落明文;桥 callId 与 pi 事件流 toolCallId 双向登记)。
|
|||
|
|
报告引用校验 `validate_report_citations()`:**报告中出现的每个 callId 必须真实存在,
|
|||
|
|
否则判 `forged_citation` 物理失败**(伪造成果防线,安全性质不可降级)。
|
|||
|
|
|
|||
|
|
**凭证语义的已知限制与 P1 处置(2026-09-02)**:凭证由编排器从事件流**事后签发**,
|
|||
|
|
真实 Pi 进程内的 LLM 无法获知其值——若简报强制要求标注 callId,真实引用必然被误判 forged
|
|||
|
|
(设计张力,P1-IMPL-NOTES 遗留风险 #2)。P1 处置 = **任务简报不再要求标注凭证**,并明示
|
|||
|
|
「凭证由围墙外签发、你不可获知、不得编造;出现不存在的凭证编号即判伪造成果整轮失败」。
|
|||
|
|
即引用从「强制」降级为「出现即必须是真」的防伪绊线;报告数字的可靠性改由
|
|||
|
|
「唯一数据来源 = inbox 只读快照」保证。伪造 callId 识别能力不变
|
|||
|
|
(`test_forged_callid_rejected` 保持绿)。
|
|||
|
|
|
|||
|
|
## 审计
|
|||
|
|
|
|||
|
|
- 完成时 1 条 `TOOL agent.fallback.propose`(成败都写,power=P1):rationale 携带
|
|||
|
|
runId / queryDigest(sha256 前 16 位,不落原话明文)/ stopReason / steps / elapsedSec /
|
|||
|
|
citationCheck 计数 / runDir / reportPath;`evidence_refs=["fallback-run:<runId>"]`。
|
|||
|
|
- 每个工具事件 1 条 `TOOL tool.run`(actor=`pi-fallback:<runId>`,power=P0)——
|
|||
|
|
GovConsole「工具调用」视图零改动即可见。
|
|||
|
|
|
|||
|
|
## 边界(P1 明确不做)
|
|||
|
|
|
|||
|
|
1. 一切写操作(fs_write/shell_run/aps_invoke/checkpoint_create 不登记);
|
|||
|
|
2. 确认卡执行路径(`agent.fallback.execute` 不登记、不实现);
|
|||
|
|
3. pi 进程内实时工具 / HTTP 桥回调(aps_query 只做启动时快照,不新增网络监听面);
|
|||
|
|
4. 后台/异步兜底与结果回投(同步 + 90s 硬上限是有意取舍);
|
|||
|
|
5. 桌面打包 / sidecar 集成(路径已可配置,sidecar.cjs 未动);
|
|||
|
|
6. SSE 实时转发 pi 事件、token 预算闸(以输出字节数近似)、端口级网络限制、junction/8.3 短路径防护;
|
|||
|
|
7. S2(已登记意图执行失败的兜底)、S3/S9 写路径、真实 pi 端到端冒烟(需带 key 环境,见
|
|||
|
|
`poc/pi-fallback/P1-VALIDATION.md` 未验证清单)。
|
|||
|
|
|
|||
|
|
## 验证
|
|||
|
|
|
|||
|
|
`tests/golden/test_fallback_lane.py` 13 例全确定性(fake runner 注入 +
|
|||
|
|
tmp_path 隔离 + 清 LLM env,不依赖真实 node/pi/网络/LLM):开关关原行为不变 /
|
|||
|
|
开关开 propose 成功 / 未登记意图仍拒绝 / 审计链不断 / 三重熔断显式失败 /
|
|||
|
|
伪造凭证判败 / runner 异常显式失败 / 默认关三态 / 路径越界拦截 / 运行时不可用回话术。
|