☰
基于Go的AI Agent流水线:从商品图自动生成淘宝详情页
2026/11/4 4:30:50 网站建设 项目流程

做电商中后台工具这几年,我最常被运营拉去处理的问题,不是数据报表,而是上新流程里无穷无尽的复制粘贴。商品图拍完,素材交到运营手里,后面还有一长串等着:起标题、拆卖点、写详情描述、整理规格参数、再把文字排版进详情图。这个流程最反直觉的是,最花时间的往往不是拍照本身,而是把图片信息翻译成文字,再把文字组装成页面。所以我用 Go 搭了一条 AI Agent 流水线,从 1 张商品图直接生成整套淘宝详情页:视觉理解 Agent 先把商品图读成结构化字段,文案 Agent 再基于这些字段生成标题、卖点和描述,最后由渲染模块拼装成可直接上架的页面。这篇文章会把整体架构、Prompt 设计、稳定性处理和实际踩到的坑拆开讲。适合正在做商品上架自动化、电商内容工具或 AI Agent 流程编排的工程师;如果你只想看一个不依赖重框架的 Go 多阶段 Agent 流水线实例,也能在这里找到参考。

1. 上新成本不在拍照,而在“信息翻译”

单看“一张图生成详情页”这句话,很多人会觉得难点在 AI 模型的选择上。我自己做完后发现,真正的难点是流程本身。

我先算过一笔账。一个普通 SKU 的上架,运营要做的事包括:从主图里确认出颜色、款式、容量等关键属性;从场景图里总结出三五个卖点;把这些信息组成标题,并且要符合各平台的字符限制和关键词位置习惯;整理规格参数表;最后还要打开设计工具,把卖点和文字一块块拼进详情图。如果是多类目、多店铺的运营团队,这套动作每天要重复几十甚至上百次。拍摄和修图早就被当作重资产环节供起来了,文案和排版反而一直处于“今天不加班就完不成”的状态。为什么?因为拍照可以批量外包,但“看图-理解-组织文案-排版”这套工作,在此之前一直没法拆。

从信息形态去看,这个问题会更清楚。商品图是图像形态的信息,标题是短文本形态,详情页是图文混排的视觉形态。从图到详情页,其实要经历三次转换:图片信息转换成结构化商品字段,结构化字段转换成电商文案,文案加上图片素材再渲染成页面。每一次转换都有明确的验证标准:字段抽取准不准、文案有没有超长、渲染出来排版完不完整。所以这件事天然就是一条流水线,而不是“一个大型模型”该干的事。

当时也有同事尝试过“直接让大模型写详情页文案”。效果很不稳定,原因很直观:一张图里能有效确认的信息本来有限,模型一旦要去写长文案,很容易把规格参数编出来,把标题写成一个没有平台语感的超长中文句子。单点模型没有中间校验,错误一旦发生,会直接传导到最终页面里。而拆成流水线之后,每一步之间都可以插入检查点:视觉字段不确定,先标成待人工确认;文案不合规,生成后被拦截重跑;渲染出错,也不影响已经校验过的结构化数据。错误被隔离在某一层,修复成本就低很多。

所以最终我没有去做一个“超级 Agent”,而是搭了一条严格的前后级联流水线:图片预处理 → 结构化抽取 → 规则校验 → 文案生成 → 合规校验 → 渲染导出。前一级的输出就是后一级的输入,每一级都有明确的失败出口。接下来几个章节,我顺着这个链路一个个讲。

2. 流水线整体设计:Go 做编排,Agent 只做“翻译官”

2.1 数据模型先于 Agent 设计

很多开发人员一上来就挑模型、写 Prompt,忽略了最重要的一件事:这条流水线要传递什么数据。我先定义了一个统一的上下文结构体,所有阶段都基于它工作:

type ProductContext struct { SKU string ImageURL string RawAttrs *RawAttrs // 视觉 Agent 抽取出的原始字段 Normalized *Attrs // 规则引擎校准后的商品属性 Copy *CopyOutput // 文案 Agent 的输出 RenderFiles []string // 渲染出的文件路径 }

这个结构体的好处是,每个阶段都可以独立 mock:测试视觉 Agent 时,我手工构造 RawAttrs;测试文案 Agent 时,我又用固定的 Normalized 数据,完全不依赖前一阶段。真正跑批处理时,每个阶段还能把上一阶段的结果落盘,中途失败可以直接断点续跑。管道结构我做的非常简单,就是一个 Step 切片:

type Step func(ctx context.Context, pc *ProductContext) error type Pipeline struct { steps []Step } func (p *Pipeline) Use(s Step) { p.steps = append(p.steps, s) } func (p *Pipeline) Run(ctx context.Context, pc *ProductContext) error { for i, step := range p.steps { if err := ctx.Err(); err != nil { return err } if err := step(ctx, pc); err != nil { return fmt.Errorf("step %d failed: %w", i, err) } } return nil }

下面是阶段划分和校验职责的表:

阶段输入输出关键校验
视觉抽取imageURLRawAttrsJSON Schema 校验、枚举值校验
规则校准RawAttrsNormalized必填项、字段统一、待确认标记
文案生成NormalizedCopyOutput长度、敏感词、属性一致性
渲染导出Copy 与图片素材HTML/长图资源完整性、目录幂等

2.2 为什么用 Go 做编排,而不是再包一层 Agent 框架

选 Go 有几个很现实的原因。第一是部署简单,最终编译出一个二进制文件,丢到服务器上配合定时任务就能批量跑,不需要为 Python 环境维护一堆依赖;第二是并发好写,多个 SKU 需要同时处理时,goroutine 加 errgroup 就能把并发控制做得明明白白;第三是 context 机制天然适合流水线,一个外层 context 可以统一控制整个批处理的超时和取消;第四是要真正解析模型返回的 JSON,Go 的强类型结构体处理 schema 异常比动态语言直观很多。

批量处理时我用的模式很简单:

var eg errgroup.Group eg.SetLimit(5) // 控制同时调用模型并发的数量 for _, sku := range skuList { sku := sku eg.Go(func() error { return pipeline.Run(ctx, &ProductContext{SKU: sku}) }) } if err := eg.Wait(); err != nil { log.Printf("batch failed: %v", err) }

这里要注意,Go 的并发很强大,但真正的瓶颈往往在模型 API 的速率限制和上下文超时上,所以一定不能无限开 goroutine。eg.SetLimit 相当于一个信号量,会把同时飞出去的请求限制在一个安全水位。

为什么没直接套用现成的 Agent 框架?因为这套流程的决策路径是完全确定的:先看图、再写文案、最后渲染。我不需要让模型自己去规划“下一步该干什么”。框架里的 tool calling、记忆、动态 Planner 在这里不仅用不上,还会让调试变难。人工排查问题时,我最需要的是一眼看到每个阶段的输入输出,而不是钻进框架内部去追链路的执行记录。所以我的结论是:如果业务流程能预先画清楚,直接用代码编排最可控;框架留给那些真正需要模型自主决策的复杂场景。

每条流水线都会遇到部分失败。我让每个 SKU 之间的处理互相隔离,一个 SKU 在视觉阶段失败,就把它标记成 failed 并继续跑下一个,绝不让整个批次停下。这个理念贯穿了后面的很多设计。

3. 第一棒 Agent:怎么把一张商品图变成结构化字段

3.1 图片送进模型前,先做“瘦身”

视觉 Agent 是整条流水线的第一棒,负责把图片翻译成结构化字段。我一开始想着图片越清晰越好,直接把原图丢给模型,结果发现成本高、响应慢,准确率也没有明显提升。后来做了两件事:先把图片下载到本地,把最长边压到 512 像素、JPEG 压缩质量控制在 85 左右,再通过 URL 传图;原图则保留到渲染阶段再用。对大多数商品图来说,512 像素足够视觉模型识别颜色、造型、包装文字这些关键信息,而 token 消耗能省掉一大部分。

如果你处理的商品有中文包装,强烈建议在 Prompt 里写一句“请优先读取包装上的中文文字,比如品牌名、净含量、规格”,这会明显提升抽取准确率。视觉模型的 OCR 能力通常不错,但如果你不明确要求,它可能只关注构图和颜色,把包装上的硬信息丢掉。

3.2 Schema 输出,以及“不确定清单”的设计

视觉抽取的结果统一要求 JSON,结构类似这样:

{ "category": "口红", "color": "正红", "finish": "哑光", "spec": "3.8g", "tags": ["显白", "持久"], "uncertain": [] }

Prompt 里我明确写死了三条规则:第一,只输出图片里能看到的信息;第二,图片里确认不了的字段不要猜,把字段名放进 uncertain 数组;第三,category 必须从运营维护的枚举清单里选,不允许自创类目名。

第三点很关键。如果模型随意自创 category,后面的规则引擎和文案模板都会出问题。我见过最典型的例子是模型把“口红”写成了“唇部彩妆”,类目统计一下全乱了,文案写出来味道也不对。让模型从封闭枚举里选,虽然看着限制多了,但对后续环节是最大的自由度。

uncertain 数组则是我整条流水线里最重要的设计之一。单张商品图的有效信息是有限的,净含量、成分、保质期这些信息往往不在图上,如果让模型硬猜,后续文案就会编出一堆错误信息。把不确定字段显式列出来,等于让模型把“不知道”这件事敞开了说。规则引擎在校准阶段看到 uncertain,会自动把对应字段标记成“待运营补充”,后续人工界面里只需要补几个字段,而不是重新看一整张图。

3.3 Go 侧严格校验与带原因的重试

模型返回的内容不能盲目信任。在 Go 里,我先用 encoding/json 解码,再手动校验 category 是否在枚举集合里、必填颜色字段是否存在。一旦解析失败,我会把失败原因和上一次输出一并发回模型,让它修正格式:

func (v *VisualAgent) Extract(ctx context.Context, imageURL string) (*RawAttrs, error) { var lastErr error for attempt := 0; attempt < 3; attempt++ { content, err := v.call(ctx, buildPrompt(imageURL, lastErr)) if err != nil { if !retryable(err) { return nil, err } waitWithBackoff(ctx, attempt) lastErr = err continue } attrs, err := parseRawAttrs(content) if err != nil { lastErr = fmt.Errorf("解析失败: %v. 原始输出: %s", err, content) continue } return attrs, nil } return nil, fmt.Errorf("visual extract failed: %w", lastErr) }

这个“把解析失败原因带回 Prompt”的技巧,比重试盲跑高效得多。模型第二次看到“JSON 解析失败,原因是非期望的空字符串”之后,通常就会老老实实输出一个合法 JSON。不过要注意,对于 400 这类请求错误,以及内容策略拦截,不要重试,重试只会白白浪费 token。429 和 5xx 这类才适合退避重试。

这里也要说一句,视觉抽取是整条流水线里成本最高的部分,所以我在数据模型里保留了 RawAttrs 的落盘位置。同样的图片 URL 只要抽过一次,就缓存下来,下次不再重复调用。

4. 第二棒 Agent:文案不是“写”,而是“在规则里生成”

4.1 输入是已校准的属性,不是自由发挥的背景故事

很多 AI 文案工具效果差,是因为它让模型自由发挥。自由发挥的文案漂亮,但不合规、不合类目结构,后期修改成本非常高。我的做法是,把规则校准后的商品属性作为硬约束塞进 Prompt:

商品属性: - 类目:口红 - 颜色:正红 - 质地:哑光 - 规格:3.8g - 卖点标签:显白、持久 要求: 1. 不要新增任何属性中不存在的成分、功效或规格信息 2. 标题控制在 60 个字符内 3. 卖点输出 3~5 条,每条不超过 20 个字符 4. 不要使用绝对化用语

加上最后一条要求非常具体,也顺带解决了标题结构问题。在电商场景里,标题一般习惯是“品牌词 + 品类词 + 核心属性 + 差异化卖点”。我把这个结构写进了 Prompt,让模型生成的标题不是一段抒情散文,而是一个能直接上架的信息组合。

文案 Agent 的输出也是一个结构化 JSON:

{ "title": "XYZ 正红哑光口红 3.8g 显白持久", "selling_points": ["哑光雾面不拔干", "正红色显白", "持妆不斑驳"], "description": "整体描述段落……", "seo_keywords": ["哑光口红", "正红口红"] }

4.2 校验器:让规则成为代码,而不是祈祷

Prompt 里写了规则,模型未必 100% 遵守。所以我在 Go 侧写了一个校验器,每条规则都是硬校验:

type CopyValidator struct { MaxTitleLen int } func (v CopyValidator) Validate(c *CopyOutput, attrs *Attrs) []string { var errs []string if len([]rune(c.Title)) > v.MaxTitleLen { errs = append(errs, "标题超长") } if !strings.Contains(c.Title, attrs.Color) { errs = append(errs, "标题缺少颜色属性") } if len(c.SellingPoints) < 3 { errs = append(errs, "卖点不足3条") } for _, w := range forbiddenWords { if strings.Contains(c.Title, w) || strings.Contains(c.Description, w) { errs = append(errs, "命中敏感词: "+w) } } return errs }

比如标题超长就建议重新生成;卖点不足三条也重新生成;如果文案里出现一个不等于原图字段的新规格数字,我也会把它当错误处理。这里的要点是,校验失败后不要盲跑,把错误原因拼进 Prompt,最多再重试两次,超过次数就进入人工确认队列。一次完整生成加一次修正重试,大多时候就够了;无限制重试只会让 token 成本迅速爆掉。

合规这块要单独说。电商文案里有一些明显的禁区词,比如“最好”“第一”“绝对”这类绝对化表述,还有各种夸大功能的说法。我维护了一个敏感词清单,让 Prompt 里明确避开,同时在校验器里做二次过滤。这看起来是笨办法,但很管用:Prompt 负责降低命中概率,校验器负责把漏网之鱼抓出来。如果命中敏感词,我不会直接删词,而是整段重生成,因为直接删词容易把句子改得不自然。

4.3 给人工留一个“补录和修改”的闸口

文案生成不是终点,后面还要过人工这一关。在管理前端,我给运营准备了一个预览面板,把结构化属性、标题、卖点、描述都展示出来。运营可以在面板里直接改,也可以针对 uncertain 字段做补充。改动保存后,重新渲染详情页即可,不需要再从视觉阶段跑一遍。这个设计看着简单,却让整套流水线从“全自动”变成了“半自动但可用”。原因很现实:平台的促销词、品牌要求、临时活动信息,模型永远无法替你判断,只有人才知道这次上架想重点讲什么。

我把人工确认放在文案生成之后、渲染导出之前,原因也很简单:如果先渲染再人工改,改一点就要重新拼一次页面;在文案阶段改,改完一次性渲染,成本低很多。

5. 落地环节:模板渲染、图片组装、可上架产出

5.1 先决定输出:HTML 详情页还是长图

文案和字段都确定后,最后一步是把内容渲染成详情页。这里要先选输出形态。电商环境里,有些后台支持直接导入 HTML 详情页,但部分平台或渠道需要 750 像素宽的长图。我的做法是保留 HTML 作为源头版本,需要长图时再通过无头浏览器对 HTML 做整页截图。这样做的原因是 HTML 可维护:运营改了文案,重新渲染一份就好;长图一旦生成,改一个字都要重新截一次图,太费事。

如果你也想用长图,不建议直接用 Go 的图像库去画文字,中文字体渲染和换行处理会让你非常痛苦。用 HTML 加上无头浏览器截图,能省掉大量排版功夫。

5.2 用 html/template 拼详情模板

Go 标准库里的 html/template 在这里刚刚好。它不仅支持模板嵌套,还会默认对变量做转义,模型生成的内容本身就不可控,这个安全特性很有价值。我按照详情页常见结构做了一个模板,区域包括头图、标题、卖点、规格表、描述:

<div class="detail" style="width:750px;background:#fff;padding:20px;"> <img src="{{.MainImage}}" style="width:100%; display:block;" /> <h1 style="font-size:30px; color:#222;">{{.Title}}</h1> <ul style="list-style:none; padding:0;"> {{range .Points}}<li style="font-size:20px; color:#444; margin:8px 0;">{{.}}</li>{{end}} </ul> <table style="width:100%; border-collapse:collapse; margin-top:20px;"> {{range $key, $value := .Specs}} <tr> <td style="padding:8px; background:#f8f8f8;">{{$key}}</td> <td style="padding:8px;">{{$value}}</td> </tr> {{end}} </table> <div style="margin-top:20px; font-size:16px; line-height:1.8;">{{.Description}}</div> </div>

渲染代码也很简单,核心就是执行模板并写到与 SKU 绑定的目录里:

func RenderDetail(sku string, data DetailData) (string, error) { t, err := template.New("detail").Parse(detailTpl) if err != nil { return "", err } var buf bytes.Buffer if err := t.Execute(&buf, data); err != nil { return "", err } path := filepath.Join(outputDir, sku, "index.html") if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return "", err } return path, os.WriteFile(path, buf.Bytes(), 0644) }

5.3 图片素材要提前推到 CDN

渲染时有一个特别容易踩的坑:模板里的图片路径。如果我把图片写成相对路径,例如images/demo.jpg,导入到部分后台后,图片会因为资源相对路径不一致而裂掉。我最终的方案是:所有图片先压缩、裁剪成需要的尺寸,上传到 CDN,模板里统一用完整的 HTTPS URL。这样无论 HTML 落在哪个预览环境,图片都能正常加载。

图片处理本身我用的是标准库的 image 包,缩略图、裁剪、格式转换都足够用。这里也有一个经验:商品图如果原始是手机拍摄的竖图,最好先检查 EXIF 方向,再做缩放,不然渲染出的头图可能是倒着的。Go 的 image 包需要把 EXIF 旋转处理一下,不然很容易在“缩略图正常但详情页头图翻转”这种问题上浪费两个小时。

5.4 导出包的幂等设计

真正上架时,我产出的不是一个 HTML 文件,而是一个完整目录包:SKU 子目录下包括 index.html、images 目录、和一份记录属性字段的 JSON 文件。JSON 文件也是给后续对接商品系统准备的,字段已经全部结构化,接后台接口时可以少做很多字段映射。

目录名按 SKU 固定,重复渲染会覆盖同一个 index.html,不会在输出目录里累积一层层垃圾文件。这个幂等设计是我在跑了十几次测试之后才补上的。刚开始没有固定路径,每次重跑都生成一个带时间戳的文件夹,运营根本分不清哪个是对的。后来改成 SKU 为唯一标识,渲染结果永远只有一个,旧文件被新版本覆盖,预览链接也保持稳定,整个流程的可维护性一下子就上来了。

6. 把流水线跑稳:重试、限流、人工闸口和成本控制

6.1 错误分类和重试策略

任何接模型 API 的流水线,第一关就是错误处理。我的原则很简单:能重试的错误才重试,不值得重试的尽快失败。

错误类型是否重试处理方式
429 限流是指数退避,等待后重试
5xx 上游错误是短暂等待后重试
400 请求错误否检查 Prompt 或参数
内容策略拦截否需要人工调整 Prompt
JSON 解析失败是把解析错误带回 Prompt 修正
超时是减少输入或增加单次超时

我给每个阶段都设了单独的上下文超时:视觉抽取最长 60 秒,文案生成 45 秒,渲染 30 秒,而整个批处理的外层 context 是 10 分钟。这样做的意义是,某个 SKU 在视觉阶段卡住时,不会拖死整个批次的剩余订单。超时失败的 SKU 会进入待人工复核队列,而不是被无限重试。

6.2 并发限制和状态恢复

前面说过我用 errgroup 的 SetLimit 控制并发数。根据我用的模型 API 的速率限制,我一般把并发定在 3 到 5 之间。并发太高,限流错误会迅速上升,反而拖慢整体进度;并发太低,几张图还好,几百张图就会等得让人怀疑人生。

另一个很重要的点是状态恢复。每个 SKU 在数据库里存一个 status,我用的状态很简单:pending、extracting、copywriting、rendering、done、failed、manual_review。流水线处理时,如果中途服务重启或单个 SKU 报错,下次处理能根据状态从断点继续。特别是视觉抽取的结果一定要落盘缓存,因为它是整条流水线里最贵的环节。我做过一个测试,服务重启后不重跑视觉 Agent,直接复用之前抽取的 RawAttrs,处理一批 100 个 SKU 的时间几乎少了一半。

6.3 人工闸口:不是扣分项,是稳定器

很多人做 Agent 流水线,追求的是“全自动”,我的经验恰好相反:保留一个轻量的人工确认节点,整套流水线反而更稳。我把人工确认做成了一个简单页面,运营看到的是标题、卖点、规格表和详情页预览,可以改字段,也可以一键返回重新生成。确认通过之后才进入渲染导出。

这个人工节点承担了两个作用:一是补充视觉 Agent 无法确定的信息,比如净含量、品牌卖点;二是拦截文案中的审核风险。模型无法预知平台当下的活动规则和品牌偏好,而运营只需要扫一眼,几秒钟就能做判断。相比完全自动生成后又反复返工,这个节点节省的时间成本非常可观。

6.4 Token 成本控制的四个习惯

最后聊成本。跑这种流水线,最大的成本来自图片模型和长文案模型的 token 消耗。我总结出四个特别管用的习惯:图片提前压缩到 512 像素内,能省掉一大截视觉 token;同样的图片 URL 加模型版本做缓存,重复处理时不重复调用;Prompt 尽量短,把属性字段用结构化文本塞进去,而不是用一大段自然语言描写;明确限制输出长度,给模型设置合理的 max_tokens,避免它输出一段没完没了的废话。这四个习惯加起来,实测能让单品的处理成本下降一半以上。

回看整套流水线,最让我受益的不是某个模型选得好,而是把流程边界划清楚。Agent 只做翻译,规则和校验全部由代码接管,人工只处理真正需要人的判断那部分,这才是这套系统能长期稳定跑下去的原因。如果你也要做类似的事情,我的建议是先把中间数据结构定好,再迭代每一步 Agent,最后再考虑优化成本;顺序反了,后面每一步都会很痛苦。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询