Agentic 资源管理
本文参考 Anthropic 的 Writing effective tools for agents、Model Context Protocol 规范、NVIDIA 的 Traditional RAG vs. Agentic RAG、Atlan 的 Agent Context Layer vs RAG、OpenAI 的 Agents SDK Tools、LangGraph 的 Memory / Checkpoint,结合自己的实际开发经验整理而成。
在 Context Engineering:Agent 上下文的三层防线 中,提到了上下文管理的 ISOLATE 层:把工作流状态、版本化事实、领域规则等搬到上下文窗外,让模型只通过受控接口与外部系统交互。Agentic 资源管理就是这一层的一种具体实现:它定义了事实如何以受控、可审计、可版本化的方式被 Agent 引用和读取。
1. Agentic 资源管理概述
Agentic 资源管理是一套让 Agent 把“事实”当成外部版本化工件来引用的设计:
- 每个事实都有稳定的逻辑 ID;
- 每次读取都返回版本/hash 等 provenance;
- 模型只被允许读取证据,不能越过授权层直接修改 Resource 的内容;
- 具体的 Agent 状态转换等逻辑由外部系统负责、完全不出现在资源层中。
NVIDIA 在 Traditional RAG vs. Agentic RAG 里把传统 RAG 比作“快速查找”,把 Agentic RAG 比作“动态知识获取”。Atlan 在 Agent Context Layer vs RAG 里进一步指出:RAG 检索相似块,而 context layer 在检索开始前先解析定义、血缘和访问策略。Agentic 资源管理和后者非常类似。
| 方式 | 模型拥有什么 | 事实由谁保管 | 可审计性 | 适合场景 |
|---|---|---|---|---|
| 硬编码 prompt | 静态文本 | 模型/提示词 | 差 | 快速 demo |
| RAG | 语义相似检索到的文本块 | 向量库 + 原始文档库 | 中等 | 开放域、非结构化知识问答、探索性阅读 |
| Agentic Resource | 结构化引用与查询参数 | 外部注册表 + 带版本/hash 的受控工件 | 高 | 业务规则、产品目录、价格表、审批策略等结构化、需权威来源的业务事实 |
Agentic 资源管理的核心设计是:把事实从模型上下文里剥离出来,由独立的资源层负责解析、版本控制和授权。
2. 具体设计
逻辑资源 ID 与注册表
让模型直接用文件名称、数据库连接串或表名读取,可能会出现路径漂移、文件名暴露之类的问题。可以引入一层逻辑资源注册表,使用 pricing.region-rules 或 product.taxonomy 之类的稳定 ID 映射到具体文件或数据库。
MCP 的 Resources 规范也遵循类似思路:服务器用 URI 暴露数据,客户端不直接操作底层存储,只通过协议读取。
注册表里除了路径,还可以顺带声明资源的其他属性:
media_type:文本、YAML、SQLite、JSON Schema 等;role:upstream_snapshot、semantic_evidence 等;authorities:该资源对哪些事实具有什么强度(authoritative / supporting / discovery_only);enabled:Agent 运行时能否加载该资源。
下面是一个简单的例子:
schema: resource-registry.v2
resources:
rules.parse-request:
path: resources/rules/parse-request.md
media_type: text/markdown
role: canonical_rulebook
authorities:
- facts: [rule.behavior]
strength: authoritative
data.product-catalog:
path: resources/data/product-catalog.db
media_type: application/vnd.sqlite3
role: upstream_snapshot
adapter: sqlite.catalog.v1
profiles.domain-queries:
path: resources/runtime/domain-query-profiles.yaml
media_type: application/yaml
每个资源都有逻辑 ID、路径、媒体类型、角色和事实授权强度。资源层把逻辑 ID 解析到实际路径,检查路径不越出 resource_root,并返回 hash、version、size 等 provenance。模型通过只读工具按 ID 读取,永远不会看到真实文件路径。
为了控制上下文,大资源不会一次性塞进模型窗口。只读工具的 resolve 动作对大文档只返回章节列表,必须再用 section 动作指定一个标题才能读取正文。这迫使模型在需要证据时主动选择范围,而不是被动接受整本规范。
声明式查询剖面
Agent 经常需要查外部数据,但直接让它写 SQL 或自由构造查询条件很危险:模型会拼错字段、选错表、把不同语义混在一起。而且还有可能为了不同语义的查询写一大堆工具,比如一个查订单之类的场景,模型看到的工具列表可能如下:
query_order_status
query_inventory_count
query_product_price
如果新增新的查询,可能又要添加新工具了。一个更稳妥的做法是:把常见查询预写成模板,只让模型填少数安全参数。
这就是声明式查询剖面(query profile)的核心思想。它可以理解为是一组“受控查询模板”。Agent 使用这个模板发起请求后,Runtime 引擎负责将 Agent 传入的参数与 Profile 中内容一起,组合成实际的 SQL 之类的查询语句进行查询。这样,新增查询只需要增加 YAML profile,不需要自己写新的工具了。
profile 的具体内容
一个 query profile 通常包含下面的内容:
- query_id:模板标识,如
user_orders或product_price; - data_source:查哪个数据源,对应注册表里的逻辑资源 ID;
- adapter:用哪个适配器读这个数据。不同数据源查法不同(SQL、API、文件),adapter 负责把 profile 的声明式配置翻译成具体查询;
- fixed_constraints:每次查询都固定的过滤条件,模型不可改;
- allowed_parameters:模型可以传入的参数白名单;
- parameter_templates:把模型输入渲染成安全的查询片段,例如把
region="EU"渲染成region_code = 'EU',而不是直接拼进 SQL; - activation:什么条件下自动触发这个查询。
等等。这些参数根据具体业务设计即可。
一次调用的完整流程
Agent 只有一个通用工具:
domain_query(query_id, parameters)
它不需要知道数据库在哪、用什么 adapter、怎么拼 SQL。假如现在有下面的 Profile:
query.user_orders.v1:
data_source: order_db
adapter: sql.readonly.v1
fixed_constraints: { is_deleted: false }
allowed_parameters: [user_id, status]
parameter_templates:
status: ["status = '{value}'"]
activation: { when: intent == "check_order" }
模型根据 Profile 的指引如下调用工具:
domain_query(
query_id="query.user_orders.v1",
parameters={"user_id": "U123", "status": "shipped"}
)
然后 Runtime 内部(这个需要根据实际的业务来实现)会按以下步骤处理:
- 找 profile:根据
query_id找到query.user_orders.v1; - 校验参数:检查
parameters里的字段都在allowed_parameters白名单里,拒绝未知参数; - 渲染模板:用
parameter_templates把参数值变成安全查询片段。例如status="shipped"→status = 'shipped'; - 组合条件:把
fixed_constraints和渲染后的参数合并成完整过滤条件; - 选数据源和 adapter:根据
data_source找到逻辑资源,根据adapter找到对应适配器; - 执行查询:adapter 把条件翻译成具体查询(如 SQL)并执行;
- 返回结果:把 candidates、来源、provenance 返回给模型。
Runtime 实际执行的 SQL 类似:
SELECT * FROM orders
WHERE is_deleted = false
AND user_id = 'U123'
AND status = 'shipped'
模型不能改 is_deleted 条件,不能访问别的表,不能写子查询或 join。查询引擎只返回候选、来源和 provenance。
下面是一个更复杂的例子:
query.product.standard_sku.v1:
enabled: true
data_source: product_catalog
adapter: sql.readonly.v1
fixed_constraints:
lifecycle: active
category: standard_sku
allowed_parameters: [size_mm, material]
parameter_templates:
size_mm:
["size_mm = {value}", "size_mm BETWEEN {value}-0.1 AND {value}+0.1"]
material: ["material_code = '{value}'"]
activation: { when: intent == "lookup_sku" }
这个 profile 限定:只查 lifecycle=active、category=standard_sku 的记录;模型只能通过 size_mm 和 material 传入参数,且查询引擎会按模板把输入渲染成安全的查询片段。4
query profile 适合查询模式稳定、参数有限的场景。如果查询需求高度发散(比如模型需要临时 join 三张表、按任意字段过滤或者更灵活地搜索一些内容),那么更适合暴露 schema 元数据 + 受限查询接口,让模型在知道资源大致架构的情况下自己构造查询或者调用工具。
工具表面的刻意狭小
无论底层有多少资源、多少 profile,模型看到的工具应该尽可能少。典型情况下只有:
resource_read:按逻辑 ID 读取资源;domain_query:按 query_id 执行预定义查询。
domain_query 的 description 要明确说:“This tool only returns candidates and provenance; it never authorizes selection or persists state.” 这样模型只能查证据,不能做其他乱七八糟的事情。
这样做有两个好处:一是模型查找资源更加简单、更不容易调用错工具或者传错参数;二是每次 API 调用传输的 tool schema 更小,更省 token。