8.0 KiB
Agent 循环实现
中文 · English
目标
K1412 循环把模型补全 API 转变为一个可追责的编码 Agent。它最核心的规则很简单:文字声明不能证明工作真的发生过。文件、命令、报告和测试都必须由本次运行中成功的工具事件提供证据。
当前事件元数据使用以下标识:
- 策略:
agent-loop-v3; - 调度器:
safe-parallel-v1; - 上下文策略:
recent-visible-v1。
这些标识是实验契约的一部分,不是营销版本号。只要行为变化可能影响评测结果,就应更新对应标识。
运行状态机
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 会:
- 只接受 system、developer、user 和 assistant 消息;
- 从旧的助手消息中移除已渲染的
<details type="tool_calls">块,避免把 UI 标记再次送入模型上下文; - 前置 K1412 系统契约;
- 最多附加八条持久用户记忆作为上下文,并明确说明它们不是更高优先级的指令;
- 从新到旧遍历可见消息,直到达到该模型的字符预算;
- 记录被丢弃的旧消息数量。
这一策略有意保持简单且可检查。它目前不会总结旧的对话分支、检索语义化工作区上下文,也不会估算不同提供方的具体 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 统计会被保留,但尚未转换成统一成本模型。
这些都是有意保留的实验方向,记录在实验方法中。