从 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 逻辑,而是:
- 用户声明一份资源配置(数据):ES 连接信息、index 名称、mapping、可查询字段、权限策略;
- 系统预置一套通用代码(平台能力):受限查询执行器、schema 到 tool 的生成器、agent runtime 的推理循环;
- 之后所有新 index、新字段,都是“填配置 / 注册资源”接入,而不是“写脚本 / 写 bot 逻辑”接入。
用户提供的是数据,而非业务代码:
- ES 连接配置(host、凭证引用,而不是明文 auth)
- index 的 mapping(字段名、类型、是否可检索、是否可返回)
- 资源权限策略(谁能查、能查哪些字段、limit 上限)
- 一句自然语言:“搜索 id 为 12345 的订单”
系统已有的是平台代码,只写一次:
- 一个受限通用 ES 查询执行器(只接受结构化、安全边界内的查询参数)
- 一个 schema → tool 生成器(把 mapping 投影成 function calling 的 JSON Schema)
- agent runtime 的推理循环(ReAct / function calling / tool calling)
- 凭证托管、审计、限流、结果脱敏和人工确认机制
一旦这个机制建立,新增一个数据源的成本会大幅下降。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 loop 关心这一步该调用什么工具
- Agent Runtime 还要关心工具从哪里来、schema 怎么生成、哪些字段能查、哪些结果能返回、这次调用之后是否要沉淀为记忆或能力
换句话说,ReAct 是 agent 的推理循环;本文方案更像是 agent runtime 的能力管理系统。它把“一次工具调用”扩展成“资源接入、工具治理、受控执行、经验沉淀”的完整链路。
下面的接入流程,就是把这个“能力管理系统”展开成几个具体模块。
三、接入流程:资源目录 + 长期记忆视角
下面把整个流程拆开,看资源目录、记忆、工具、上下文工程和 MCP 分别在其中承担什么角色。
① 资源注册
用户第一次说“这是我的 ES,index 叫 orders,mapping 长这样”——Runtime 不应该把它当作普通对话历史随手塞进 memory,而应该注册成一个资源描述,进入受治理的资源目录(resource registry / connection profile / tool catalog)。
真正落地时,首先要把“长期可用的信息”拆成三类:
- 对话上下文:当前会话里刚说过什么,通常短期有效;
- 长期记忆:用户偏好、常用资源、历史选择等可复用经验,通常不直接保存敏感凭证。比如:“用户经常查 orders”、“默认只看 id/amount/created_at”。
- 资源目录:外部系统的连接、schema、权限、凭证引用,跨会话有效,但需要治理。比如 ES endpoint、index、mapping 属于资源目录;凭证不进入普通 memory,而是进入 secret manager / credential vault,资源目录里只保存凭证引用。
资源配置不是普通记忆:它和“用户偏好”“学到的结论”都能长期复用,但治理等级不同。资源配置需要权限、审计、版本和密钥管理;偏好记忆则更多服务于个性化召回。
**资源配置本身也可以通过对话生成。**若给了 endpoint + index + 权限,用户侧只要自然地说一句“我有个 ES,地址 http://es.internal:9200,索引叫 orders,id 是 keyword、amount 是数字……”,流程就变成:
- LLM 从对话抽取结构化资源(slot filling / 结构化抽取)→ 生成内部资源描述草稿;
- 系统校验与自发现 → 调 ES 的
_mappingAPI 拉回真实 schema,检查字段、类型、权限; - 写入前回显确认(“我理解你的 ES 是:索引 orders,字段 id(keyword)/amount(double)……对吗?”)→ 用户纠正或确认;
- 确认后写入资源目录;可选地把用户偏好写入长期记忆。
这样从用户视角仍然接近零代码、零文件:“配置”和“对话”是同一件事的两面,用户只管说,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 完成推断:
- 该调用
es_orders_search; id是 keyword 类型 → 适合等值过滤;- 生成参数
{"filters": [{"field": "id", "op": "eq", "value": "12345"}]}。
这就是“推断”的真相:LLM 并不需要预先知道 ES DSL 怎么写,它只需要知道有哪些资源、每个资源有哪些字段能力、哪些参数被允许,就能把自然语言翻译成结构化调用。
④ 通用执行器运行 + 结果回写
通用 ES 查询执行器拿到结构化参数后,先做权限检查、字段白名单校验、limit 限制、审计记录,再把参数编译成 ES DSL,执行查询,返回 hits,最后由 LLM 总结给用户。
更进一步:如果这次交互让 bot 学到“用户常查 orders index”或“用户关心 amount 字段”,Runtime 可以把这些作为候选偏好记忆沉淀下来,下一次无需用户重复说明。
但这里要再次强调一下,记忆回写也要分级:
- 偏好类事实可以在低风险场景下自动写入,或通过策略控制;
- 资源配置、schema、权限、连接信息必须经过校验和确认后更新;
- 凭证、token、密码不进入普通长期记忆,只保存到专门的密钥系统。
⑤ 从临时调用到可复用能力
这里还要补一个关键边界:不是每次查询之后,都应该把这条查询语句沉淀成 skill 或 script。
一次自然语言查询通常只是临时意图,比如“搜索 id 为 12345 的订单”。这类请求执行完以后,最多沉淀为偏好记忆或使用统计。如果每次查询都自动生成一个 script 或 skill,很快就会产生大量低质量、重复、过窄、难治理的能力碎片。
更合理的做法是分层沉淀:
- 单次查询:只执行,不沉淀成能力;
- 高频查询模式:沉淀为偏好、查询模板或 saved query,例如“按订单 id 查订单详情”;
- 稳定且可复用的操作流程:沉淀为 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,或直接使用已有的 Elasticsearch MCP server / 平台提供的 Agent Builder MCP server;
- Agent Runtime 通过 MCP 协议动态发现 tools,工具名称、描述和 input schema 由 server 自描述;
- Runtime 负责连接 MCP server,并继续承担记忆管理、资源召回、工具路由、权限提示、审计和推理循环。
这样可以进一步降低平台侧接入成本:你不再为 ES 单独写一套工具协议,而是使用 MCP server 暴露出来的标准工具。
对应到前文结构,如果使用 MCP,tool schema 和部分受控执行器能力可以由 MCP server 提供;Agent Runtime 仍然负责选择哪个 server/tool、如何注入上下文、如何做权限提示、如何记录记忆与 trace。
不过,MCP 解决的是工具协议标准化和动态发现,不是自动消灭所有工程问题。实际落地时仍然需要配置 server、凭证、权限策略、网络边界、日志审计、危险操作确认,以及对工具调用结果的脱敏和大小限制。
所以更准确地说:MCP 让“接入一个外部系统”从自定义集成变成标准协议集成;业务侧依然可以不写代码,但平台侧仍要治理这些工具如何被发现、调用和约束。
五、模式的泛化价值
一旦 Runtime 具备“声明式资源 → 动态工具 → LLM 推理 → 受控执行”的能力,可接入的对象就不限于 ES。任何能暴露 schema、操作语义和权限边界的系统,都可以用类似机制接入:
- 关系型数据库(MySQL / PostgreSQL 的表结构、索引、只读查询策略);
- 内部 HTTP API(用 OpenAPI 描述接口、参数、鉴权和副作用等级);
- 对象存储(bucket、prefix、文件类型、读取/写入权限);
- 消息队列、推荐服务、指标平台、工单系统……
这里的重点不是“只要有 schema 就够了”。Schema 只描述字段和参数,真正可用的 agent 接入还需要:
- 操作语义:这是查询、创建、删除,还是会触发外部副作用;
- 权限模型:谁能调用、能看哪些字段、能查多大范围;
- 安全策略:是否需要人工确认、是否要脱敏、是否要限流;
- 错误语义:调用失败、超时、无权限、结果过大时如何反馈给 LLM。
这也正好呼应最小 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 的机制理解成三层:
- Memory 记住事实和偏好:用户是谁、偏好什么、常用哪些上下文;
- Skill 记住做法和流程:遇到某类任务时应该按什么步骤执行、注意哪些坑、如何验证结果;
- 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 走向生产,至少要补上这些边界:
- 凭证不进普通 memory:memory 里最多保存
credential_ref,真正的 token / password 进入 vault。 - LLM 不直接执行任意 DSL:LLM 只填写受限结构化参数,执行器负责把参数编译成 DSL。
- 工具 schema 最小权限:只暴露当前用户可调用、当前任务需要的字段和操作。
- 高风险操作要 human-in-the-loop:删除、写入、发消息、生产变更、资金相关操作都应要求确认。
- 结果要脱敏和限量:限制返回字段、返回条数、payload 大小,避免把过多数据塞回上下文。
- 资源召回要隔离租户和环境:不能因为语义相似就召回另一个团队、另一个环境的资源。
- 记忆回写要分级:偏好可自动,资源配置要确认,权限和凭证必须走治理流程。
这些约束会让方案少一点“魔法感”,但多很多可落地性。真正可靠的 Agent Runtime,不是让 LLM 获得无限自由,而是让它在清晰 schema、明确权限和可审计执行器之内发挥推理能力。
小结
要让一个接了大模型的 bot 自动推断、调用外部资源,并形成可复用的长期上下文,需要的不是单纯更聪明的模型,而是一个把外部世界以声明式资源形式接入,并让 LLM 在受控 schema 上做推理的 Runtime:
- 用户声明资源(ES 连接、index、mapping、权限策略),系统校验后写入资源目录;
- Runtime 根据资源 schema、用户权限和安全策略投影成动态工具;
- LLM 结合召回的资源与工具 schema,把自然语言翻译成结构化调用;
- 通用执行器受控运行,完成权限检查、DSL 编译、审计、脱敏和结果返回;
- Runtime 只把低风险偏好沉淀为长期记忆,资源配置和凭证走更严格的治理链路。
因此,本文讨论的不是替代 ReAct loop,而是在 ReAct loop 外围补上资源、工具、权限、记忆和演化机制。
代码仍然存在,但它从“每个业务场景写一份”变成“平台通用能力写一次”。能力扩展靠注册资源、声明策略、接标准协议完成,这才是 Agent Runtime 在长期演进中的真正价值。
