别把 URL 当产物:给多工具 Agent 加一份文件交接清单

0 阅读6分钟

如果一个 Agent 工具只返回下面的结果,你的下一步会怎么写?

{"success": true, "url": "https://files.example.com/result.png"}

下载,再继续?也许够做演示,但它没告诉你:这张图是哪一版、适合放在哪里、隔天还拿不拿得到。example.com 只是示意地址,不是真实资源。

这次想讨论的不是“让 Agent 更聪明”,而是一个容易被 UI 流畅度掩盖的接口问题:产物身份不应该与访问地址绑定。

入口在合并,文件仍要交接

Adobe 9 月 2 日发布的 Adobe for Slack,让用户在对话中调用创作与文档工具,并利用相关上下文继续编辑。官方也保留了进入专业应用做更细控制的路径。公告

这对减少窗口切换有意义。但作为下游系统开发者,我们还得处理另一个问题:A 工具产出的文件,B 工具究竟消费了哪一份?下面是通用设计,不是对 Adobe 内部实现的判断。

设想一个内容流水线:生成源海报 → 导出横幅与竖版 → 放入文档 → 预填平台。链接、文件版本和平台资源 ID 如果都叫 url,最后一层出问题时,很难知道该重试哪一步。

产物身份与临时访问分开,最后由接收端核对

持久化引用,临时解析地址

我会先区分两个类型:

type ArtifactRef = {
  artifactId: string;
  revision: string;       // 不可变内容版本
  mediaType: string;
  sha256: string;         // 对实际产物字节计算
  bytes: number;
};

type AccessGrant = {
  url: string;
  expiresAt?: string;
  resolvedArtifactId: string;
  resolvedRevision: string;
};

ArtifactRef 可以进任务状态;AccessGrant 由有权限的执行器临用时取得。不要让模型编签名、拼下载地址,或把可访问地址持久化成唯一索引。

这不是强迫所有系统用某家对象存储。S3 的两个规则只是很好的例子:开启版本控制后,同一 key 可以有不同 version ID;预签名链接的有效性还受底层凭证生命周期影响。版本机制链接时效

因此,cover.png 可以只是定位名,r2 才是交付选择。后续解析应该坚持 r2:找不到就报错,不能偷偷回落到最新版。

临时签名链接可能携带访问能力,记录日志时也应脱敏。让执行器取得必要权限,不等于把所有链接贴进模型上下文。

把消费写成一次有结果的接收

下面是 TypeScript 接口示意,刻意把外部依赖留在适配器中。downloadAndInspect 应由受控代码完成实际下载、计算摘要、检测格式;绝不能只是复述工具给出的元数据。

type Observation = { sha256: string; bytes: number; mediaType: string };
type Receipt = { artifactId: string; revision: string; checkedAt: string };

async function receive(
  ref: ArtifactRef,
  resolve: (ref: ArtifactRef) => Promise<AccessGrant>,
  downloadAndInspect: (url: string) => Promise<Observation>
): Promise<Receipt> {
  const grant = await resolve(ref);
  if (grant.resolvedArtifactId !== ref.artifactId ||
      grant.resolvedRevision !== ref.revision) {
    throw new Error("WRONG_VERSION");
  }
  const got = await downloadAndInspect(grant.url);
  if (got.sha256 !== ref.sha256 || got.bytes !== ref.bytes) {
    throw new Error("BYTES_MISMATCH");
  }
  if (got.mediaType !== ref.mediaType) {
    throw new Error("FORMAT_MISMATCH");
  }
  return {
    artifactId: ref.artifactId,
    revision: ref.revision,
    checkedAt: new Date().toISOString()
  };
}

这段函数没有自动重试,也没有把失败转换成“换个最新版试试”。它的返回值只表示该接收检查通过,不代表图片没有错字、PDF 事实正确,更不代表有权公开。

上线还需要可信清单、身份鉴别、下载目标限制、大小上限和文件安全检查。不能允许不可信模型任意指定内部网络 URL,再让服务端照单下载。

一份源稿,几份导出,不能共用一个身份

横图不是竖图缩个尺寸那么简单。裁切会改变主体位置,字幕可能掉到边界之外;导出 PDF 时字体和分页也会变化。

所以源稿和派生件分别存,并记录派生输入:

{
  "artifactId": "poster-portrait",
  "revision": "r5",
  "sourceRefs": [{"artifactId": "master-artwork", "revision": "r4"}],
  "transform": {"name": "portrait-layout", "revision": "layout-2"},
  "constraints": {"width": 1080, "height": 1440}
}

这里的数值是示例规格,不是某平台的统一要求。实际发布前要核对目标平台当日要求。

源稿派生多种规格,某一导出失败不应触发全量重做

派生关系有两个用处。第一,竖版裁坏时只重做这一支。第二,源稿换版后能找出受影响产物,而不是把所有文件都改名叫“新版”。历史已交付包应保留自己的选版,不随当前源稿漂移。

不要急着追求通用 DAG 引擎。先把每份导出“来自哪一版、用了哪个转换配置”记清楚,简单列表也能解决很多接力问题。

重试的是故障层,不是整场对话

下面这些情况看起来都是“文件没拿到”,处理却完全不同:

  • 链接过期:核验权限后重新解析同一个产物版本。
  • 指定版不存在:报告缺件,不生成替代品冒充原件。
  • hash 不匹配:隔离结果,排查覆盖、传输或定位错误。
  • 规格不匹配:重新导出对应派生件。
  • 平台上传超时:先查询或回读已有上传结果,未确认前不要重复提交。

同时设有限重试和总超时,别让“重新签一个地址”变成无限循环。日志记录 artifactIdrevision、阶段、错误码与耗时,不必记完整敏感 URL。

平台压缩图片后,字节通常可能变化。原文件传输校验与平台页面验收应分开:前者认内容摘要,后者认资源标识以及真实页面上的可用内容。不能要求重新编码后的 CDN 文件仍保留原 hash。

最小测试集比漂亮的成功路径更重要

至少覆盖这些输入:同名不同版、链接到期、缺一件、多一件旧稿、下载半截、MIME 与实际内容不一致、正确文件被裁坏、上传成功但响应丢失。

其中“正确文件被裁坏”必须落到内容验收:原图字节一致,依然可能不是正确的视觉交付。程序检查不是替代视觉检查的借口。

你可以先落地一个很小的闭环:生产端输出清单,消费端比对版本与字节,平台端保存资源 ID 和页面回读结果。不要只统计生成工具成功率,也统计缺件率、错版率和失败后的重做范围。

AI 员工要接手的是工作,不只是链接

这类文件接力也是我们做 Tipkay 时关注的问题。面向小微企业、一人公司和小团队的岗位助手,可以各自带着经验、Skill、MCP 和流程,把内容继续推进到配图、排版、文件与发布准备。

本文的类型和接收函数是通用实现建议,不声称 Tipkay 已采用这套内部结构。产品定位之外,工程判断仍应独立成立:工具输出的文件必须能被下一步明确接收。

一个人的生意,也能有一支专业团队。对开发者来说,可以先把这句话落实成一个接口约束:success 后面,不能只跟着一个来历不明、明天可能失效的 URL。