fix(agent): 附件图作为参考图传给图片生成模型
设计点:这里不是简单加一个数组字段。它把“本地附件 / 下载文件 / bash 产物”统一收敛到 workspace asset ID,避免 image_generation 工具同时承担文件读取、上传、引用解析三种职责。cap 对齐 Go 侧 MaxReferenceImages,也把错误边界前移
source_automation: “voyager-merged-pr” run_date: “2026-07-05” anchor_pr_number: 4344 pr_number: 4344 pr_title: “fix(agent): 附件图作为参考图传给图片生成模型” pr_url: “https://github.com/adastralab-ai/voyager/pull/4344 ↗” author: “BaoLei / blurname” merged_at: “2026-07-05T05:00:42Z” modules: [“backend/workers/agent/src/tools”,“backend/workers/agent/src/tools/imageGenerationTool.ts”,“backend/workers/agent/src/tools/toolDefs.ts”,“backend/workers/agent/src/tools/imageGenerationTool.test.ts”] files_changed: 3 learning_tags: [“agent-runtime”,“image-generation”,“attachments”,“referenceAssetIds”,“tool-schema”,“task-worker-boundary”] business_line: null related_prs: [4314,4318] line_stage: null open_questions: [] feishu_doc_url: “https://my.feishu.cn/docx/EzKBdAj0uoEPY6xjaADcw3rCn7m ↗”#
正文来源:飞书学习文档 ↗。以下为通过个人 Feishu API 获取并转换后的完整 Markdown 正文。
今日选择#
- PR:fix(agent): 附件图作为参考图传给图片生成模型 (#4344)
- 作者:BaoLei / blurname
- Merge 时间:2026-07-05T05:00:42Z(Australia/Melbourne 2026-07-05 15:00:42 AEST)
- 链接:https://github.com/adastralab-ai/voyager/pull/4344 ↗
- 模块:backend/workers/agent/src/tools
- 学习标签:agent-runtime, image-generation, attachments, referenceAssetIds, tool-schema, task-worker-boundary
为什么值得学#
- 它补的是 AI agent 的跨边界数据流缺口:用户上传图片原本只进入附件路径和 prompt 上下文,但没有成为 image_generation 的结构化参考输入。PR 没有重做附件系统,而是把路径先转成 workspace asset,再沿已有 Go/task-worker referenceAssetIds 管道下传。
- 改动位置很克制:最终 merge commit 只改 agent tool schema、tool executor、单测 3 个文件;没有改 IDL/codegen,也没有改 Go 和 task-worker,因为后端链路已经支持 referenceAssetIds。
- schema 描述被当成模型操作契约,而不只是类型定义:prompt 字段说明了有/无 referenceAssetIds 时怎么写;aspectRatio 明确引用图不自动决定输出比例;referenceAssetIds 明确必须来自现有 asset 或 upload_asset 返回值,不能编造。
- 前端/agent 层把失败尽量前置:MAX_REFERENCE_ASSETS 镜像 Go 的 MaxReferenceImages,让超过 10 张参考图的错误在工具输入校验阶段失败,而不是提交到 Go 后再返回 4xx。
关键代码#
1. 工具 schema 给 referenceAssetIds 建立输入契约#
// backend/workers/agent/src/tools/toolDefs.ts @ 9084f00b:218-265
const MAX_REFERENCE_ASSETS = 10;
referenceAssetIds: z
.array(z.string().min(1))
.max(MAX_REFERENCE_ASSETS)
.optional()
.describe(
"Optional existing workspace image asset IDs to use as visual references... If a needed image is only a local file ... call `upload_asset` with its path and pass the returned `asset.id`. Never invent asset IDs.",
),plaintext设计点:这里不是简单加一个数组字段。它把“本地附件 / 下载文件 / bash 产物”统一收敛到 workspace asset ID,避免 image_generation 工具同时承担文件读取、上传、引用解析三种职责。cap 对齐 Go 侧 MaxReferenceImages,也把错误边界前移。
2. executor 只透传结构化参数,保留 resumable task 语义#
// backend/workers/agent/src/tools/imageGenerationTool.ts @ 9084f00b:47-82
execute: async (
{ prompt, resolution, aspectRatio, modelId, referenceAssetIds },
{ abortSignal },
) => {
const submitBody = {
prompt,
resolution: resolution ?? DEFAULT_IMAGE_GENERATION_SETTINGS.resolution,
aspectRatio,
modelId: modelId ?? imageModelId ?? DEFAULT_IMAGE_GENERATION_SETTINGS.modelId,
referenceAssetIds,
};
return runResumableImageTask({
taskType: "image_generation",
idempotencyKey: await imageTaskIdempotencyKey("image_generation", submitBody),
submit: () => submitImageGeneration(goApiContext, submitBody),
});
}plaintext设计点:referenceAssetIds 被纳入 submitBody,因此也进入 idempotencyKey。这样同一 prompt 但参考图不同,不会错误复用同一个 pending/resumable image task;同时不破坏 #4318 引入的重试 re-attach 语义。
3. upload_asset 的路径边界扩到 /mnt/attachments#
// backend/workers/agent/src/tools/toolDefs.ts @ 9084f00b:1109-1117
export const sandboxUploadAssetToolDef = {
description: "Upload a local image file into assets.",
inputSchema: z.object({
path: z
.string()
.min(1)
.describe(
"Absolute path (/workspace or /mnt/attachments) or path relative to /workspace",
),plaintext设计点:附件不直接变成 image_generation 的文件参数,而是通过 upload_asset 进入 workspace assets。这个描述变化很小,但它把用户上传附件所在的 R2/FUSE 路径纳入现有 asset 管道。
4. 单测覆盖的是 agent-to-Go 透传,不是 UI 展示#
// backend/workers/agent/src/tools/imageGenerationTool.test.ts @ 9084f00b:106-132
const output = await tool.execute!({
prompt: "A quiet mountain lake",
aspectRatio: "16:9",
referenceAssetIds: ["asset-ref-1"],
}, toolCallOptions);
expect(submitImageGeneration).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
aspectRatio: "16:9",
referenceAssetIds: ["asset-ref-1"],
}),
);plaintext设计点:测试只守住有回归价值的边界:agent tool 输入必须进入 Go API request。它没有为工具描述文案、prompt 文案或 UI 表现加脆弱测试。
和最近学习记录的关系#
有直接关系。昨天学习的是 #4314:task-worker 把 gpt-image-2 迁到 AI Gateway/OpenAI 路由,并梳理 provider adapter、返回 bytes、SafetyError 等运行时边界。#4344 接在它上游:不是改 provider,而是让 agent runtime 把用户附件转成 referenceAssetIds,真正喂到已经可处理参考图的 Go/task-worker 管道里。
更完整的链路是:用户消息附件只注入 路径提示 → model 通过 upload_asset 把 /mnt/attachments 文件提升为 workspace asset → image_generation(referenceAssetIds) → Go image_generation task input → task-worker 对 Gemini 走 inline base64 parts,对 gpt-image-2 走 imageUrls。#4344 解决的是链路第一段的结构化参数缺失。
我会怎么吸收#
- 做 agent tool 时,优先让模型传结构化引用 ID,而不是要求模型把图片内容总结进 prompt;prompt 只能辅助表达意图,不能替代真实输入。
- 已有后端能力不要重复造:如果下游已经支持 referenceAssetIds,agent 层只需要暴露最薄的参数和正确的转换路径。
- 把模型可见的 schema description 当作产品协议写清楚:来源、上限、默认行为、不要编造 ID、引用图不决定 aspectRatio,这些比普通类型更影响实际 agent 行为。
边界/风险#
- 最终 merge commit 没有保留第一版里对 system prompt / generate-image skill 的显式流程改动,主要依赖 tool schema description 引导模型。风险是模型是否稳定先调用 upload_asset,再带 referenceAssetIds 调 image_generation,仍需要线上轨迹验证。
- 单测覆盖了透传,但没有覆盖完整 agent planning 流程:带附件输入时是否会按预期读取/上传/引用,仍主要靠手工 Test Plan 或端到端 trace。
- referenceAssetIds 是可选字段;纯文本生图行为保持不变,但也意味着“用户说参考附件”能否成功取决于模型是否选择这个参数。
飞书文档#
本文档即本次飞书保存副本。
候选说明#
今天(Australia/Melbourne 2026-07-05)窗口查到 7 个已 merge 到 main 的 PR;昨天窗口查到 11 个。因为今天已有可学习候选,没有扩大到更早日期。#4314 已在学习日志中记录,按去重规则跳过。最终选 #4344,是因为它直接补齐昨天图片生成 provider/runtime 迁移的上游 agent 参数链路;#4352 的字体加载/共享包迁移也有学习价值,#4348 的 PostHog replay 观测链路也不错,但和最近学习主题的演进关系不如 #4344 明确。