ReAct Loop
OryxOS 从零实现了 ReAct(Reason + Act)算法——ReActLoop.java 里数十行 Java 代码,没有框架委托,没有黑盒 Agent 抽象。每次迭代都是透明的、可审计的。
循环如何运转
七个步骤,最多重复 max_iterations 次(默认 10 次):
- 接收 — 用户消息通过某个渠道到达(CLI、HTTP 等)
- 追加 — 消息追加到 SQLite 里的
Session对话历史 - 构建 Prompt —
PromptBuilder组装四段式 Prompt(见下文) - 调用 LLM —
ProviderService将 Prompt 发送给配置的 Provider - 检查响应 — 判断模型返回的是工具调用还是最终答案
- 无工具调用 → 返回响应给调用方,循环结束
- 有工具调用 →
ToolExecutor执行工具,结果以结构化的工具消息回填历史,回到第 3 步
用户消息
→ 追加到 Session 历史 [SessionManager → SQLite]
→ PromptBuilder 组装 Prompt
→ ProviderService 调用 LLM [写 llm_calls 表]
→ 无工具调用 → 返回最终响应
→ 有工具调用 → ToolExecutor
→ SandboxChecker 白名单校验
→ 执行(进程内 或 JSON-RPC 转发到 MCP server)
→ 写 tool_invocations 表
→ 结果追加到历史
→ 继续循环(默认最多 10 次)Prompt 里注入了什么
PromptBuilder 按以下顺序从四个来源组装 Prompt:
| # | 部分 | 来源 | 负责方 |
|---|---|---|---|
| 1 | 系统提示词 — 该 Agent 的 AGENT.md 正文 + Bootstrap 文件(AGENTS.md、SOUL.md、USER.md) | 文件系统 | ContextLoader |
| 2 | 长期记忆 — 该 Agent 的 MEMORY.md 全文(截断至 4000 字符) | .oryxos/agents/<name>/MEMORY.md(回退到 .oryxos/memory/MEMORY.md) | MemoryService |
| 3 | 对话历史 — 最近 max_history_turns 轮,以结构化消息形式(包含此前 assistant 的工具调用和带 id 的工具结果) | SQLite sessions.messages_json | SessionManager |
| 4 | 可用工具 — 该 Agent 暴露的每个工具的 JSON Schema | ToolRegistry | ToolRegistry |
Agent 目录里的子指令(skills/*.md)和脚本(scripts/)不会被预先注入——正文会指引模型按需用 read_file / shell 读取。
Prompt 在每次迭代时重新构建。工具调用和结果以结构化的 tool_call / tool_result 消息经 Provider 传递——assistant 的工具调用与按 id 匹配的工具响应——而不是拍平成纯文本。如果工具执行结果改变了模型下一步应该做的事,重新构建的 Prompt 会把这个变化带入。
ToolExecutor
ToolExecutor 负责从 LLM 决定调用工具到结果加入对话历史之间发生的一切:
- 解析 — 从模型响应中提取工具名称和参数
- 查找 — 在
ToolRegistry中找到对应的OryxTool实现 - 校验 —
SandboxChecker对照配置的白名单验证调用 - 执行 — 内置工具在进程内执行;MCP 工具通过 JSON-RPC 转发
- 审计 — 向
tool_invocations写入一条记录(成功与否、耗时、输入、结果) - 返回 —
ToolResult作为带原始工具调用 id 的结构化工具结果消息追加到对话历史,让下一次 LLM 调用把它当作正规的工具响应,而不是自由文本
如果工具执行失败且 ToolResult.retryable 为 true,错误信息仍会追加到历史,让模型在下次迭代中尝试别的方案。
关键约束
max_iterations — 在 AGENT.md frontmatter 的 settings 里按 Agent 配置(默认 10),防止无限循环。达到上限时循环停止,返回最后一次 LLM 响应。
settings:
max_iterations: 10
max_history_turns: 20上下文窗口管理 — 长期记忆截断至 4000 字符(LongTermMemory.truncateIfNeeded()),保留最近的内容。对话历史上限为 max_history_turns 轮。两种截断都不是静默发生的:PromptBuilder 会记录实际注入的长度。
同步执行 — 整个循环是同步阻塞的。并发由 HTTP/渠道层的 Java 21 虚拟线程处理。循环内部不用 Reactor、不用 CompletableFuture、不用回调。
Spring AI 工具执行已禁用 — ChatClient 的自动工具执行被显式关闭。如果启用,工具会执行两次——Spring AI 执行一次,ToolExecutor 再执行一次。ProviderService 直接调用 chatModel.call(prompt) 并将原始 ChatResponse 交给循环处理。
// 错误 — Spring AI 会自动执行工具
chatClient.prompt(prompt).tools(tools).call().content();
// 正确 — 原始调用,循环自己检查并执行工具调用
ChatResponse response = chatModel.call(new Prompt(messages, options));核心阶段不包含的内容
以下功能规划在扩展阶段实现,核心循环里没有:
- 并行工具调用 — 单次迭代中同时调用多个工具
- 流式响应 — SSE 或 WebSocket 流式传输部分 token
- 带退避的循环级重试 — 目前 LLM 调用失败会直接把错误抛给调用方
- 分支 / 子 Agent 委托 — 核心阶段只有单 Agent 线性循环