Jason Pan

从 ReAct Loop 到 Agent Runtime:声明式资源接入与长期记忆

潘忠显 / 2026-07-09


设想这样一个场景:你告诉 bot 一个 Elasticsearch 的连接配置和一个订单索引,然后随口说一句“去搜索 id 为 12345 的订单”。整个过程里,业务接入方没有为这个 index 写任何代码,只是把一个外部资源接进了 Agent Runtime。

问题来了:Agent Runtime 要具备什么能力,才能让 bot 自动推断意图、调用正确的资源,并把“用户常用哪个索引、偏好看哪些字段”这类信息沉淀为长期记忆,在后续对话中持续可用?

本文给出的核心答案是一句话:

把“为每个资源写代码”解耦为“声明式资源描述 + 受限通用执行器 + 动态工具合成 + LLM 推理循环”。LLM 负责把自然语言映射到资源 schema 上,平台侧代码只写一次。

这里的“不写代码”不是说系统里完全没有代码,而是说:用户或业务接入方不需要为每个 index、每张表、每个 API 单独写 bot 逻辑;Agent 平台预置通用执行器、工具生成器、权限和审计机制,后续接入主要变成声明资源与配置策略。

一、为什么“业务不写代码”能成立

我们希望的不是为每个 index 写一段 search 逻辑,而是:

用户提供的是数据,而非业务代码:

系统已有的是平台代码,只写一次:

一旦这个机制建立,新增一个数据源的成本会大幅下降。Agent Runtime 相对“写死脚本的 bot”的本质优势,不是它神奇地消灭了代码,而是把代码沉到平台层,把业务变化变成声明式资源和策略配置。

二、从 ReAct Loop 到 Agent Runtime

如果从最小 agent 设计看,很多系统一开始都会落到一个 ReAct / function calling loop:

用户问题
  -> LLM 推理
  -> 选择工具
  -> 调用工具
  -> 观察结果
  -> 继续推理
  -> 回答用户

这个循环很重要,它解决的是“模型如何一步步使用工具”。但仅有这个 loop,还不足以支撑本文开头的场景。因为它默认工具已经存在、工具边界已经清楚、权限已经处理好、资源 schema 已经进入上下文。

本文讨论的 Agent Runtime,是在 ReAct loop 之上再往前走一步:

资源目录
  -> 动态工具合成
  -> 受限 tool schema
  -> 权限 / 字段治理
  -> 受控执行器
  -> LLM 推理循环
  -> 记忆回写
  -> saved query / script / skill 演化

所以二者不是替代关系,而是层次关系:

换句话说,ReAct 是 agent 的推理循环;本文方案更像是 agent runtime 的能力管理系统。它把“一次工具调用”扩展成“资源接入、工具治理、受控执行、经验沉淀”的完整链路。

下面的接入流程,就是把这个“能力管理系统”展开成几个具体模块。

三、接入流程:资源目录 + 长期记忆视角

下面把整个流程拆开,看资源目录、记忆、工具、上下文工程和 MCP 分别在其中承担什么角色。

① 资源注册

用户第一次说“这是我的 ES,index 叫 orders,mapping 长这样”——Runtime 不应该把它当作普通对话历史随手塞进 memory,而应该注册成一个资源描述,进入受治理的资源目录(resource registry / connection profile / tool catalog)。

真正落地时,首先要把“长期可用的信息”拆成三类:

资源配置不是普通记忆:它和“用户偏好”“学到的结论”都能长期复用,但治理等级不同。资源配置需要权限、审计、版本和密钥管理;偏好记忆则更多服务于个性化召回。

**资源配置本身也可以通过对话生成。**若给了 endpoint + index + 权限,用户侧只要自然地说一句“我有个 ES,地址 http://es.internal:9200,索引叫 ordersid 是 keyword、amount 是数字……”,流程就变成:

  1. LLM 从对话抽取结构化资源(slot filling / 结构化抽取)→ 生成内部资源描述草稿;
  2. 系统校验与自发现 → 调 ES 的 _mapping API 拉回真实 schema,检查字段、类型、权限;
  3. 写入前回显确认(“我理解你的 ES 是:索引 orders,字段 id(keyword)/amount(double)……对吗?”)→ 用户纠正或确认;
  4. 确认后写入资源目录;可选地把用户偏好写入长期记忆。

这样从用户视角仍然接近零代码、零文件:“配置”和“对话”是同一件事的两面,用户只管说,LLM 和 Runtime 负责把自然语言落成受治理的资源描述。

② 动态工具合成

任务开始前,Runtime 根据用户权限和当前任务,从资源目录与长期记忆里召回相关资源,自动生成给 LLM 调用的工具描述。工具不是为每个 index 手写的,而是由 schema、权限和策略投影生成的。

一份声明式资源描述(Runtime 内部的表示形式)长这样:

# 声明式资源描述 —— 用户提供或系统自发现后生成
resources:
  - name: es_orders
    type: elasticsearch
    endpoint: http://es.internal:9200
    credential_ref: vault://team-a/es-orders-readonly
    index: orders
    access:
      allowed_roles: [order_ops, support]
      max_limit: 100
      readonly: true
    schema:
      id:         { type: keyword, filterable: true,  returnable: true }
      user_id:    { type: keyword, filterable: true,  returnable: true }
      amount:     { type: double,  range: true,       returnable: true }
      created_at: { type: date,    range: true,       returnable: true }
      internal_note: { type: text, returnable: false }

基于这份 schema,Runtime 生成 function calling / tool calling 定义。重点不是让 LLM 写任意 ES DSL,而是让它填写一个受限的结构化查询接口

{
  "name": "es_orders_search",
  "description": "在 orders 索引中按受限条件检索订单",
  "parameters": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "filters": {
        "type": "array",
        "items": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "field": {
              "enum": ["id", "user_id", "amount", "created_at"]
            },
            "op": {
              "enum": ["eq", "gte", "lte"]
            },
            "value": {
              "type": ["string", "number"]
            }
          },
          "required": ["field", "op", "value"]
        }
      },
      "fields": {
        "type": "array",
        "items": {
          "enum": ["id", "user_id", "amount", "created_at"]
        }
      },
      "limit": {
        "type": "integer",
        "minimum": 1,
        "maximum": 100
      }
    },
    "required": ["filters"]
  }
}

注意关键点:工具的形态由资源 schema、权限和安全策略共同决定,而非由业务代码决定。LLM 看到的可用工具,来自 Runtime 对资源目录的召回与投影。

实际实现中,op 也应该按字段类型和字段能力继续裁剪:keyword 字段只开放 eq,数字和日期字段才开放 gte / lte;执行器还要做二次校验,不能只依赖模型按 schema 自觉选择。

③ LLM 自动推断

用户说“搜索 id 为 12345 的订单”。Runtime 先召回与当前用户、当前任务相关的 ES 资源(上下文工程),LLM 结合 schema 完成推断:

这就是“推断”的真相:LLM 并不需要预先知道 ES DSL 怎么写,它只需要知道有哪些资源、每个资源有哪些字段能力、哪些参数被允许,就能把自然语言翻译成结构化调用。

④ 通用执行器运行 + 结果回写

通用 ES 查询执行器拿到结构化参数后,先做权限检查、字段白名单校验、limit 限制、审计记录,再把参数编译成 ES DSL,执行查询,返回 hits,最后由 LLM 总结给用户。

更进一步:如果这次交互让 bot 学到“用户常查 orders index”或“用户关心 amount 字段”,Runtime 可以把这些作为候选偏好记忆沉淀下来,下一次无需用户重复说明。

但这里要再次强调一下,记忆回写也要分级:

⑤ 从临时调用到可复用能力

这里还要补一个关键边界:不是每次查询之后,都应该把这条查询语句沉淀成 skill 或 script

一次自然语言查询通常只是临时意图,比如“搜索 id 为 12345 的订单”。这类请求执行完以后,最多沉淀为偏好记忆或使用统计。如果每次查询都自动生成一个 script 或 skill,很快就会产生大量低质量、重复、过窄、难治理的能力碎片。

更合理的做法是分层沉淀:

也就是说,Runtime 应该维护一条从“临时调用”到“可复用能力”的升级链路:

自然语言请求
  -> 结构化 tool call
  -> 偏好记忆 / saved query
  -> script / workflow
  -> skill / agent capability
  -> self-evolving skill

这个升级过程不应该完全自动。系统可以根据调用频率、成功率、用户重复表达、人工确认等信号生成候选能力,但真正写入 script 或 skill 前,最好经过用户确认、代码审查或权限审批。否则 agent 会把偶然行为固化成长期能力,后续反而更难维护。

以 Nous Research 的 Hermes Agent 为例,其公开文档强调 built-in learning loop:会从经验中创建 skill,并在使用中改进 skill。它把 skill 视为一种 procedural memory:不是单纯记录“用户说过什么”,而是记录“这类任务下次应该怎么做”。它的学习闭环可以概括成:

执行任务
  -> 观察成功路径 / 用户纠正 / 错误恢复 / 多步工具调用
  -> 抽取可复用经验
  -> 创建或更新 skill
  -> 后续相似任务召回 skill
  -> 根据新反馈继续 patch / evolve

因此,Hermes 给本文的启发是:skill 可以演化,但演化对象应该是稳定流程,而不是每一次临时查询。比如“查一个订单 id”只是一次 tool call;但“定位订单异常:查订单、关联用户、拉取日志、总结原因、按固定格式输出排查报告”这类稳定流程,就有机会被沉淀成 skill。

前提仍然是有触发条件、写入门禁和治理边界。否则 self-evolving skills 会从“持续改进”变成“持续污染”。

四、想更少写平台代码:走 MCP

如果不想把 ES 工具协议和执行细节都放进 Runtime,可以考虑 MCP(Model Context Protocol)

这样可以进一步降低平台侧接入成本:你不再为 ES 单独写一套工具协议,而是使用 MCP server 暴露出来的标准工具。

对应到前文结构,如果使用 MCP,tool schema 和部分受控执行器能力可以由 MCP server 提供;Agent Runtime 仍然负责选择哪个 server/tool、如何注入上下文、如何做权限提示、如何记录记忆与 trace。

不过,MCP 解决的是工具协议标准化和动态发现,不是自动消灭所有工程问题。实际落地时仍然需要配置 server、凭证、权限策略、网络边界、日志审计、危险操作确认,以及对工具调用结果的脱敏和大小限制。

所以更准确地说:MCP 让“接入一个外部系统”从自定义集成变成标准协议集成;业务侧依然可以不写代码,但平台侧仍要治理这些工具如何被发现、调用和约束。

五、模式的泛化价值

一旦 Runtime 具备“声明式资源 → 动态工具 → LLM 推理 → 受控执行”的能力,可接入的对象就不限于 ES。任何能暴露 schema、操作语义和权限边界的系统,都可以用类似机制接入:

这里的重点不是“只要有 schema 就够了”。Schema 只描述字段和参数,真正可用的 agent 接入还需要:

这也正好呼应最小 Agent Runtime的边界设计:Runtime 只管循环、记忆、资源召回、工具路由和安全门禁,具体能力通过外部资源与协议注入。能力的外延可以扩展,Runtime 本身保持稳定。

六、和 OpenClaw / Hermes 这类 Runtime 的关系

从公开文档看,OpenClaw、Hermes Agent 这类系统都在探索类似的 Agent Runtime 形态;本文聚焦的是其中一个更窄的能力切片。

OpenClaw、Hermes Agent、Codex、Claude Desktop、Cursor 等系统,本质上都不只是“把大模型包成聊天窗口”,而是在模型外面提供一层运行时能力:agent loop、session、memory、skills、tools、MCP 接入、权限和执行环境。可以把它们理解为 Agent 的操作系统或宿主环境。

其中 Hermes Agent 的特点更偏“自我改进”:其官方文档强调 built-in learning loop,会从经验里创建 skill、在使用中改进 skill,并把 memory、skills、MCP 和工具执行环境结合起来。它提供了一个很好的参照:skill 不是一次性写完的静态说明,而是可以被 Runtime 创建、召回、修改和持续优化。

可以把 Hermes 的机制理解成三层:

  1. Memory 记住事实和偏好:用户是谁、偏好什么、常用哪些上下文;
  2. Skill 记住做法和流程:遇到某类任务时应该按什么步骤执行、注意哪些坑、如何验证结果;
  3. Tool / MCP 负责真实执行:调用浏览器、终端、外部 API、MCP server 或本地脚本完成动作。

这也解释了为什么“写 skill”和“声明式工具定义”不是对立关系。Skill 解决的是 agent 面向任务的经验组织和召回;声明式工具定义解决的是可验证、可授权、可审计的执行入口。一个成熟的 Runtime 往往需要两者:上层 skill 负责“什么时候用、怎么用、有哪些边界”,底层 tool 负责“确定性地把事情做成”。

本文讨论的“声明式资源接入”可以理解为 OpenClaw / Hermes-style Agent 这类 Runtime 的一个核心能力切片

OpenClaw / Hermes-style Agent Runtime
  ├─ agent loop
  ├─ session / memory
  ├─ skills / tools / MCP
  ├─ resource registry                          [本文聚焦]
  ├─ dynamic tool synthesis                     [本文聚焦]
  ├─ permission / audit / execution sandbox     [本文聚焦]
  ├─ controlled executor                        [本文聚焦]
  └─ optional self-evolving skills              [相关延伸]

七、最小设计骨架(伪代码)

# 1) 资源目录:用户声明一次,系统校验后长期可用
resource_registry.save(Resource(
    name="es_orders",
    type="elasticsearch",
    endpoint="http://es.internal:9200",
    credential_ref="vault://team-a/es-orders-readonly",
    index="orders",
    schema={
        "id": {"type": "keyword", "filterable": True, "returnable": True},
        "user_id": {"type": "keyword", "filterable": True, "returnable": True},
        "amount": {"type": "double", "range": True, "returnable": True},
        "created_at": {"type": "date", "range": True, "returnable": True},
    },
    policy={"readonly": True, "max_limit": 100},
))

# 2) 动态工具合成:schema + policy -> function calling 定义
def synthesize_tool(resource, user):
    authorized_schema = filter_fields_by_permission(resource.schema, user)
    return {
        "name": f"{resource.name}_search",
        "parameters": build_safe_query_schema(authorized_schema, resource.policy),
    }

# 3) 推理循环:召回资源 -> LLM 推断 -> 受控执行器
resources = resource_registry.recall_relevant(user_msg, user)
tools = [synthesize_tool(r, user) for r in resources]

for turn in react_loop(user_msg, tools, llm):
    if turn.is_tool_call:
        resource = route_tool_to_resource(turn.tool_name)
        args = validate_against_tool_schema(turn.args)
        result = generic_es_executor(resource, args, user)  # 权限、限流、审计、脱敏都在这里
        turn.feed(result)
    else:
        reply(turn.text)

# 4) 记忆回写:只把低风险偏好沉淀为长期记忆
preference = extract_low_risk_preference(user_msg, result)
memory.maybe_save(preference, policy="low_risk_or_confirmed")

核心只有一处是面向 ES 查询的真正执行代码——generic_es_executor。其余新增资源主要靠数据驱动:注册资源、投影工具、召回上下文、受控执行。

这里自然会冒出一个问题:既然 skill 可以像 Hermes 那样持续演化,generic_es_executor 这类执行器能不能也让 LLM 自动生成?

答案是:可以让 LLM 生成候选实现,但不能让它在运行时临时生成代码并直接执行生产查询。执行器是安全边界,负责权限、字段白名单、limit、DSL 编译、脱敏和审计;一旦写错,风险不是“回答差一点”,而是越权查询、泄露数据或打爆外部系统

更合理的方式是把它放进受控代码生成流水线:

资源 schema / 权限策略
  -> LLM 生成 executor 草稿
  -> 单元测试 / mock 外部系统测试
  -> 静态扫描 / 安全规则检查
  -> 人工 review 或策略审批
  -> 版本化发布
  -> Runtime 调用已批准 executor

也就是说,LLM 可以参与开发和迭代 executor,但生产 Runtime 只能调用已经验证、审批、版本化的执行器。

八、工程边界与安全门禁

为了让这套方案能从 demo 走向生产,至少要补上这些边界:

  1. 凭证不进普通 memory:memory 里最多保存 credential_ref,真正的 token / password 进入 vault。
  2. LLM 不直接执行任意 DSL:LLM 只填写受限结构化参数,执行器负责把参数编译成 DSL。
  3. 工具 schema 最小权限:只暴露当前用户可调用、当前任务需要的字段和操作。
  4. 高风险操作要 human-in-the-loop:删除、写入、发消息、生产变更、资金相关操作都应要求确认。
  5. 结果要脱敏和限量:限制返回字段、返回条数、payload 大小,避免把过多数据塞回上下文。
  6. 资源召回要隔离租户和环境:不能因为语义相似就召回另一个团队、另一个环境的资源。
  7. 记忆回写要分级:偏好可自动,资源配置要确认,权限和凭证必须走治理流程。

这些约束会让方案少一点“魔法感”,但多很多可落地性。真正可靠的 Agent Runtime,不是让 LLM 获得无限自由,而是让它在清晰 schema、明确权限和可审计执行器之内发挥推理能力。

小结

要让一个接了大模型的 bot 自动推断、调用外部资源,并形成可复用的长期上下文,需要的不是单纯更聪明的模型,而是一个把外部世界以声明式资源形式接入,并让 LLM 在受控 schema 上做推理的 Runtime

  1. 用户声明资源(ES 连接、index、mapping、权限策略),系统校验后写入资源目录;
  2. Runtime 根据资源 schema、用户权限和安全策略投影成动态工具;
  3. LLM 结合召回的资源与工具 schema,把自然语言翻译成结构化调用;
  4. 通用执行器受控运行,完成权限检查、DSL 编译、审计、脱敏和结果返回;
  5. Runtime 只把低风险偏好沉淀为长期记忆,资源配置和凭证走更严格的治理链路。

因此,本文讨论的不是替代 ReAct loop,而是在 ReAct loop 外围补上资源、工具、权限、记忆和演化机制。

代码仍然存在,但它从“每个业务场景写一份”变成“平台通用能力写一次”。能力扩展靠注册资源、声明策略、接标准协议完成,这才是 Agent Runtime 在长期演进中的真正价值。