# 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. 从旧的助手消息中移除已渲染的 `
` 块,避免把 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)中。