179 lines
8.0 KiB
Markdown
179 lines
8.0 KiB
Markdown
# Agent 循环实现
|
||
|
||
中文 · [English](agent-loop.md)
|
||
|
||
## 目标
|
||
|
||
K1412 循环把模型补全 API 转变为一个可追责的编码 Agent。它最核心的规则很简单:文字声明不能证明工作真的发生过。文件、命令、报告和测试都必须由本次运行中成功的工具事件提供证据。
|
||
|
||
当前事件元数据使用以下标识:
|
||
|
||
- 策略:`agent-loop-v3`;
|
||
- 调度器:`safe-parallel-v1`;
|
||
- 上下文策略:`recent-visible-v1`。
|
||
|
||
这些标识是实验契约的一部分,不是营销版本号。只要行为变化可能影响评测结果,就应更新对应标识。
|
||
|
||
## 运行状态机
|
||
|
||
```mermaid
|
||
stateDiagram-v2
|
||
[*] --> BuildContext
|
||
BuildContext --> RequestModel
|
||
RequestModel --> ValidateCalls: 存在工具调用
|
||
RequestModel --> CheckCompletion: 没有工具调用
|
||
ValidateCalls --> Schedule
|
||
Schedule --> Execute
|
||
Execute --> RecordEvidence
|
||
RecordEvidence --> RequestModel
|
||
CheckCompletion --> RequestModel: 证据不足且仍有预算
|
||
CheckCompletion --> Completed: 证据充分
|
||
CheckCompletion --> Unverified: 证据不足且重试预算耗尽
|
||
Completed --> [*]
|
||
Unverified --> [*]
|
||
```
|
||
|
||
每次顶层运行都有唯一的 `run_id`。所有事件都包含用户、对话、单调递增序号、时间戳、事件类型和净化后的载荷。
|
||
|
||
## 上下文策略
|
||
|
||
`ContextPolicy.prepare` 会:
|
||
|
||
1. 只接受 system、developer、user 和 assistant 消息;
|
||
2. 从旧的助手消息中移除已渲染的 `<details type="tool_calls">` 块,避免把 UI 标记再次送入模型上下文;
|
||
3. 前置 K1412 系统契约;
|
||
4. 最多附加八条持久用户记忆作为上下文,并明确说明它们不是更高优先级的指令;
|
||
5. 从新到旧遍历可见消息,直到达到该模型的字符预算;
|
||
6. 记录被丢弃的旧消息数量。
|
||
|
||
这一策略有意保持简单且可检查。它目前不会总结旧的对话分支、检索语义化工作区上下文,也不会估算不同提供方的具体 token 切分方式。这些都是未来可以实验的方向。
|
||
|
||
## 模型契约
|
||
|
||
`ModelSpec` 是以下配置在服务端的唯一事实来源:
|
||
|
||
- 公开模型 ID 和提供方模型 ID;
|
||
- 提供方选择;
|
||
- 是否支持思考以及显示标签;
|
||
- 提供方支持时所使用的 reasoning effort;
|
||
- 最大输出 token 数;
|
||
- 最大循环迭代次数;
|
||
- 上下文字符预算。
|
||
|
||
Luna、Terra 和 Sol 是三个不同的本地模型,各自以布尔值表示是否支持思考;它们不是同一模型的三档推理强度。DeepSeek V4 Pro 使用 DeepSeek 提供方,启用思考,并设置 `reasoning_effort=max`。
|
||
|
||
如果提供方协议要求,Runtime 会在多轮工具调用之间保留 `reasoning_content`,但不会把隐藏推理发布为用户可见内容。
|
||
|
||
## 工具目录
|
||
|
||
工作区工具:
|
||
|
||
- 获取状态,列出、读取、搜索、写入和补丁修改文件;
|
||
- 执行前台命令;
|
||
- 检查 Git 状态与 diff;
|
||
- 启动、轮询和取消后台进程。
|
||
|
||
Runtime 状态工具:
|
||
|
||
- 更新每个对话的计划;
|
||
- 记住、回忆和遗忘持久用户记忆。
|
||
|
||
委派工具:
|
||
|
||
- 使用角色、任务和明确的写入策略启动一个有边界的子 Agent。
|
||
|
||
工具 schema 使用 `additionalProperties: false`,让格式错误的模型参数尽早失败。参数会在策略检查前完成规范化。内容正文和补丁正文不会写入公开运行事件的载荷。
|
||
|
||
## 基于意图的工具选择
|
||
|
||
根循环并不总是发送全部工具。轻量级请求分类器会判断任务是否涉及产物、执行、源码、报告、比较、进程、Git、记忆或委派。筛选后的工具目录可以降低模型进行工具决策的难度,同时保留核心工作区工具。
|
||
|
||
这只是启发式路由,不是权限控制。Gateway 仍然是实际的强制执行边界。
|
||
|
||
## 调度器
|
||
|
||
每次模型响应最多请求八个工具调用。调用按原始顺序解析,并划分为连续的组:
|
||
|
||
- 标为 `parallel_safe` 的工具并发执行;
|
||
- 写操作和其他非并行工具串行执行;
|
||
- 只读子 Agent 可以并行运行;
|
||
- 拥有写权限的子 Agent 串行执行;
|
||
- 子 Agent 不能再次委派。
|
||
|
||
Gateway 还会按用户串行化工作区写操作。第二层锁非常重要,因为多个 Runtime 请求或浏览器操作可能同时访问同一个工作区。
|
||
|
||
即使并行组中的任务完成顺序不同,调度器仍会保持结果的原始顺序。
|
||
|
||
## 防御性规范化与重试保护
|
||
|
||
循环会修复少量常见的模型格式错误,然后应用策略:
|
||
|
||
- 拒绝 `python3`、`bash`、`node` 这类没有参数的交互式命令;
|
||
- 拒绝把文档或数据文件当作可执行源码运行;
|
||
- 在写入完整 Python 文件之前拒绝语法无效的内容;
|
||
- 解码弱模型生成的、重复转义的源码布局换行;
|
||
- 跳过内容完全相同且此前成功的写入;
|
||
- 在内容变化前阻止重复执行完全相同的失败写入;
|
||
- 在发生写操作或实质不同的诊断前,阻止重复执行没有变化的失败命令;
|
||
- 对同一批次里的重复命令去重;
|
||
- 限制生成长度、迭代次数和工具批次大小。
|
||
|
||
在执行层,Bash 启用 `pipefail` 并返回真实退出码。前台工具默认最长运行 900 秒;后台进程具有明确的启动、轮询和取消生命周期。
|
||
|
||
## 基于证据的完成门禁
|
||
|
||
循环会从用户请求中推导所需证据。
|
||
|
||
| 请求类型 | 最低证据要求 |
|
||
| --- | --- |
|
||
| 具体产物 | 一次成功的文件写操作,以及之后一次成功的验证 |
|
||
| 执行/测试/分析 | 至少一次成功的执行或检查 |
|
||
| 源码请求 | 单独的可执行源文件 |
|
||
| 报告请求 | 在执行之后单独写入的报告 |
|
||
| 基准测试/比较报告 | 从成功执行输出中复制的精确数值测量 |
|
||
|
||
验证可以是之后的文件读取、Git diff/status、命令执行或后台进程轮询。失败的命令会一直保持未解决状态,直到后续执行成功。
|
||
|
||
如果模型过早尝试完成任务,Runtime 会发出 `completion.rejected`,向模型返回聚焦的恢复指令,并继续循环。经过多次被拒绝的完成检查或迭代次数耗尽后,Runtime 会返回明确的未完成结果,而不是把未经验证的声明包装成成功。
|
||
|
||
## 委派
|
||
|
||
根 Agent 可以委派一个边界明确的独立任务。子 Agent 会获得:
|
||
|
||
- 一个角色和精确任务;
|
||
- 最多八次迭代的缩减预算;
|
||
- 不包含委派工具;
|
||
- 默认仅有只读工具,除非明确请求了写权限。
|
||
|
||
子 Agent 事件与根运行共用同一事件流,但 `depth` 会增加。当前实现是在单个 Runtime 进程内递归执行,不是分布式队列,也不是持久自治 worker。
|
||
|
||
## 记忆与计划
|
||
|
||
持久记忆按用户 ID 隔离。当前优先返回最近记忆,并支持可选的、不区分大小写的子字符串过滤。计划按用户和对话隔离,并且最多只允许一个 `in_progress` 项。
|
||
|
||
记忆很有价值,但系统有意保持保守:不会从每次对话自动提取事实,也暂未实现向量检索、置信度评分、过期和冲突解决。
|
||
|
||
## 运行事件模型
|
||
|
||
重要事件类型包括:
|
||
|
||
- `run.created`、`context.built`、`run.completed`、`run.failed`、`run.cancelled`;
|
||
- `model.requested`、`model.responded`;
|
||
- `tool.started`、`tool.completed`、`tool.batch_limited`;
|
||
- `completion.rejected`、`completion.unverified`;
|
||
- `agent.spawned`、`agent.completed`。
|
||
|
||
事件流目前用于驱动 UI 工具详情块,也是未来实现回放、评测、成本分析和 A/B 分组的基础。事件仅存储公开参数和简短摘要,不存储凭据、完整写入正文或隐藏推理。
|
||
|
||
## 已知限制
|
||
|
||
- 上下文压缩会直接丢弃旧消息,而不是总结。
|
||
- 记忆检索基于文本而不是语义。
|
||
- 工具意图分类器基于正则表达式。
|
||
- 调度器只并行连续的安全调用,且没有资源成本模型。
|
||
- Runtime 重启后,子 Agent 不会持久存在。
|
||
- 尚未实现实验分组和聚合仪表盘。
|
||
- 提供方返回的 token 统计会被保留,但尚未转换成统一成本模型。
|
||
|
||
这些都是有意保留的实验方向,记录在[实验方法](experiments.zh-CN.md)中。
|