Skip to content

编号资产管理(Agent Asset Index) ​

模块路径:backend/harness/asset/ 适用范围:Chat / Image / Video / Omni 四个 Agent 共享。 本文是「编号资产管理」子系统的总览,聚焦核心类与核心流程,用图解建立 mental model。


一、它解决什么问题 ​

Agent 在生图 / 生视频时,需要引用会话中的图片、视频、音频。如果让 LLM 直接在工具参数里填 URL,有三个顽疾:

顽疾表现
URL 幻觉LLM 编造不存在的 URL
URL 抄错长 URL 容易抄错,历史上出现过裸编号 "1" 穿透到 Provider,触发 unknown url type: '/1'
历史资产难引用之前生成的图 URL 很长,LLM 记不住、对不上

解法:给每个多模态资产分配一个稳定短编号(img_1 / vid_2 / aud_1),LLM 只填编号,后端把编号解析成真实 URL。这样 LLM 永远不碰 URL,从根上消除幻觉与抄错。


二、两个核心概念 ​

1. 编号体系 ​

每类媒体用 ASCII 前缀,各自从 1 连续编号:

类型编号格式来源
图片img_1, img_2, …历史图 + 本轮参考图 + 首尾帧,统一连续编号
视频vid_1, vid_2, …本轮上传视频在前,历史生成视频接续
音频aud_1, aud_2, …本轮上传音频

三类图片(历史图、本轮参考图、首尾帧)共享同一个 img_N 序列:历史图 img_1..M → 本轮参考图 img_{M+1}.. → 首帧 / 尾帧接续 img_N / img_{N+1}。这样 LLM 看到的是一套连续编号。

2. 两个 state 字段(务必区分) ​

字段语义生命周期内容
state.assets资产池(有哪些资产)累积(有 reducer){asset_id: {url, type, description, created_at}}
state.assets_index_map编号映射(此刻叫什么编号)每请求重建的派生映射(无累积){img_1: url, img_2: url, …}

前者是「资产真相」,后者是「编号此刻的投影」。编号映射每轮重建,所以稳定它是整个子系统设计的核心难点。


三、核心类 ​

按职责分成五组(对应后面五个流程环节):

环节核心类文件
提取入池MessageAssetExtractor / AssetCatalogbackend/harness/asset/catalog.py
本轮素材视图CurrentRequestAssetsbackend/harness/asset/current_request.py
编号生成AssetIndexBuilder / AssetIndex / ids.pybackend/harness/asset/index/
写回 stateAssetStateUpdaterbackend/harness/asset/state_update.py
展示给 LLMAssetPromptPresenterbackend/harness/asset/presentation/prompt_presenter.py
解析回 URLAssetReferenceResolver + resolve_reference_listbackend/harness/asset/index/resolver.py、reference_resolver.py
数据模型AssetRecordbackend/harness/asset/models.py

四、核心流程:五环节闭环 ​

整个子系统是一条闭环,资产从「出现」到「被 Provider 使用」必经这五步:

下面逐环节展开。

4.1 提取入池 ​

MessageAssetExtractor.extract 扫描消息历史,从三类消息里抠出图片资产:

关键约定:提取强依赖 COS URL 正则 + 图片判定。所有资产必须托管在 COS,这里才能识别。

python
# backend/harness/asset/catalog.py
COS_URL_PATTERN = re.compile(
    r"https://[a-z0-9-]+\.cos\.[a-z0-9-]+\.myqcloud\.com/[^\s\)\]\"\',]+"
)
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"}

def is_image_url(url: str) -> bool:
    path = url.split("?")[0].lower()
    return any(path.endswith(ext) for ext in IMAGE_EXTENSIONS) or "/images/" in path

description 的智能关联:生图连续生成多张图时,AIMessage.content 常是过渡寒暄(「第一张完成!接着第二张」),无法区分单张。所以提取器先建 tool_call_id → 描述 映射,优先用生图工具的 prompt 参数(更具体),content 兜底,再通过 tool_call_id 关联回对应 ToolMessage。

4.2 编号生成(核心难点:保证稳定) ​

AssetIndexBuilder.images() 把历史图与本轮图合并成一条连续 img_N 序列:

为什么要「排除已在历史的 URL」?因为 context_node 每轮都会重建编号映射,必须保证第一轮和第 N 轮给同一张图编同一个号,否则 LLM 之前填的编号就错了。

python
# backend/harness/asset/index/builder.py
def images(self, *, existing_assets, request, max_assets) -> AssetIndex:
    history = self.history_images(existing_assets, max_assets)
    current = self.current_images(
        request,
        offset=len(history.entries),
        exclude_urls=set(history.entries.values()),  # ← 按 URL 去重
    )
    return history.merge(current)

首尾帧不并入 assets(每轮由当前请求重新提供),其编号由 AssetPromptPresenter.append_image_refs 接续 img_N。

4.3 写回 state ​

AssetStateUpdater 只做两件写回动作,不重复计算编号(编号由调用方算好传入,避免首入 / 回归不一致):

  • merge_current_request_images:把本轮 default 参考图并入 state.assets 池
  • merge_index_maps:把调用方算好的各类编号映射整体写入 state.assets_index_map(每请求覆盖)

4.4 展示给 LLM ​

AssetPromptPresenter 把编号渲染成文本片段拼进 system prompt,且编号与 assets_index_map 的 key 完全一致(它复用同一个 AssetIndexBuilder,而非自己另算):

[本次会话中的图片]
以下图片出现在本次会话中,需要参考时,请使用对应编号填入 reference_images 参数(不要使用 URL):
img_1. [历史图片1],一匹可爱的小马
img_2. [参考图片1]

4.5 解析回 URL(三态校验) ​

resolve_reference_list 对 LLM 填的每一项逐项校验,无论 assets_index_map 是否为空:

python
# backend/harness/asset/index/reference_resolver.py
for ref in refs or []:
    if is_passthrough_url(ref):          # 合法 URL → 放行
        resolved_urls.append(ref)
        continue
    url = resolver.resolve_single(ref)    # 编号 → URL
    if url is not None:
        resolved_urls.append(url)
    else:
        invalid_refs.append(ref)          # 非法 → 阻断

「无论是否为空都逐项校验」这个约定,根治了历史 bug:早期 handler 在 assets_index_map 为空时跳过解析,导致裸编号穿透到 Provider。 旧中文编号(如「图片1」)的兼容由 backend/harness/asset/index/legacy_alias.py 处理,属迁移期产物。


五、三个关键设计 ​

设计 1:URL 去重 → 首入 / 回归编号一致 ​

资产池对同一 URL 幂等(AssetCatalog.merge 按 known_urls 去重),所以无论提取多少次,池子内容稳定;编号基于「并入本轮图片后的 assets」一次性算好,AssetStateUpdater 不重算。这是编号稳定性的根基。

设计 2:三态校验 → 防裸编号穿透 ​

解析端对每一项「合法 URL / 命中编号 / 非法」三分,非法直接阻断并回告可用编号,绝不穿透到 Provider。

设计 3:描述回填 → 修复历史脏数据 ​

旧代码入库的图 description 全是泛化默认值(「历史生成的图片」),因 URL 去重永不更新。提取器开了一条特殊路径:当已有图的 description 属于泛化集合且能从 messages 重新关联到准确描述时,按 asset_id 回填。详见 2026-06-22-agent-asset-two-phase-description-backfill.md。


六、一次完整调用的生命周期 ​

以「用户上传 1 张图 → LLM 调生视频工具引用它」为例:

工具返回的产物会再触发「提取入池」(tool_node 即时提取 + context_node 全量重建),形成循环。


七、源码索引 ​

关注点文件
数据模型 / asset_id 生成backend/harness/asset/models.py
资产池视图 + 消息提取backend/harness/asset/catalog.py
本轮请求素材视图backend/harness/asset/current_request.py
编号体系常量 / 前缀backend/harness/asset/index/ids.py
AssetIndex(编号→URL 映射)backend/harness/asset/index/asset_index.py
编号生成(连续编号 + 去重)backend/harness/asset/index/builder.py
编号→URL 解析backend/harness/asset/index/resolver.py
参考列表解析(三态校验)backend/harness/asset/index/reference_resolver.py
旧中文编号兼容backend/harness/asset/index/legacy_alias.py
写回 statebackend/harness/asset/state_update.py
渲染进 promptbackend/harness/asset/presentation/prompt_presenter.py
每轮重建入口backend/harness/context/node.py
即时提取(本轮工具返回后)backend/harness/agent/common/tool_node.py

八、相关文档 ​