Files
zk-data-agent/docs/agent-loop.zh-CN.md
2026-07-26 21:32:12 +08:00

8.0 KiB
Raw Permalink Blame History

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 会:

  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 请求或浏览器操作可能同时访问同一个工作区。

即使并行组中的任务完成顺序不同,调度器仍会保持结果的原始顺序。

防御性规范化与重试保护

循环会修复少量常见的模型格式错误,然后应用策略:

  • 拒绝 python3bashnode 这类没有参数的交互式命令;
  • 拒绝把文档或数据文件当作可执行源码运行;
  • 在写入完整 Python 文件之前拒绝语法无效的内容;
  • 解码弱模型生成的、重复转义的源码布局换行;
  • 跳过内容完全相同且此前成功的写入;
  • 在内容变化前阻止重复执行完全相同的失败写入;
  • 在发生写操作或实质不同的诊断前,阻止重复执行没有变化的失败命令;
  • 对同一批次里的重复命令去重;
  • 限制生成长度、迭代次数和工具批次大小。

在执行层,Bash 启用 pipefail 并返回真实退出码。前台工具默认最长运行 900 秒;后台进程具有明确的启动、轮询和取消生命周期。

基于证据的完成门禁

循环会从用户请求中推导所需证据。

请求类型 最低证据要求
具体产物 一次成功的文件写操作,以及之后一次成功的验证
执行/测试/分析 至少一次成功的执行或检查
源码请求 单独的可执行源文件
报告请求 在执行之后单独写入的报告
基准测试/比较报告 从成功执行输出中复制的精确数值测量

验证可以是之后的文件读取、Git diff/status、命令执行或后台进程轮询。失败的命令会一直保持未解决状态,直到后续执行成功。

如果模型过早尝试完成任务,Runtime 会发出 completion.rejected,向模型返回聚焦的恢复指令,并继续循环。经过多次被拒绝的完成检查或迭代次数耗尽后,Runtime 会返回明确的未完成结果,而不是把未经验证的声明包装成成功。

委派

根 Agent 可以委派一个边界明确的独立任务。子 Agent 会获得:

  • 一个角色和精确任务;
  • 最多八次迭代的缩减预算;
  • 不包含委派工具;
  • 默认仅有只读工具,除非明确请求了写权限。

子 Agent 事件与根运行共用同一事件流,但 depth 会增加。当前实现是在单个 Runtime 进程内递归执行,不是分布式队列,也不是持久自治 worker。

记忆与计划

持久记忆按用户 ID 隔离。当前优先返回最近记忆,并支持可选的、不区分大小写的子字符串过滤。计划按用户和对话隔离,并且最多只允许一个 in_progress 项。

记忆很有价值,但系统有意保持保守:不会从每次对话自动提取事实,也暂未实现向量检索、置信度评分、过期和冲突解决。

运行事件模型

重要事件类型包括:

  • run.createdcontext.builtrun.completedrun.failedrun.cancelled
  • model.requestedmodel.responded
  • tool.startedtool.completedtool.batch_limited
  • completion.rejectedcompletion.unverified
  • agent.spawnedagent.completed

事件流目前用于驱动 UI 工具详情块,也是未来实现回放、评测、成本分析和 A/B 分组的基础。事件仅存储公开参数和简短摘要,不存储凭据、完整写入正文或隐藏推理。

已知限制

  • 上下文压缩会直接丢弃旧消息,而不是总结。
  • 记忆检索基于文本而不是语义。
  • 工具意图分类器基于正则表达式。
  • 调度器只并行连续的安全调用,且没有资源成本模型。
  • Runtime 重启后,子 Agent 不会持久存在。
  • 尚未实现实验分组和聚合仪表盘。
  • 提供方返回的 token 统计会被保留,但尚未转换成统一成本模型。

这些都是有意保留的实验方向,记录在实验方法中。