
一张白底商品图丢进去几分钟后吐出一整套淘宝详情页——主图卖点、功能参数、材质工艺、场景展示、常见问题连 SEO 标题和上下架文案都给你排好。我用 Go 搭的这条 AI Agent 流水线已经在给身边几个做电商的朋友用了。这事有意思的点在于AI Agent 这个说法这两年已经被聊烂了但从能聊天到真下地干活中间隔着一条完整的工程链路。我也是踩了不少坑才把调用大模型生成文案这件小事扩展成一条从图像理解、属性抽取、内容生成到模板渲染的自动化流水线。这篇文章把这些过程和经验完整写出来想用 Go 做 AI 自动化生产的同学可以直接参考我的整体设计和代码思路。1. 为什么我要用 Go 搭这条商品图转详情页流水线先说背景。我本职工作写 Go 后端业余帮朋友打理电商店铺的视觉和文案。详情页这东西看着简单做起来极其耗时先得从商品图里抠卖点再找参考竞品然后写文案、排版、生成细节图最后还要根据不同平台的要求调整尺寸和语气。一套下来熟练的人也得一两个小时遇上新品上架高峰期一天做十套就是极限了。最开始我也试过直接用现成的 AI 工具比如把图丢给大模型让它写文案。但问题很明确单次对话式的工具没法处理整个流程。你得先判断图片里是什么商品再决定从哪些维度写卖点然后生成不同模块的文案最后还要拼成一套结构完整的详情页。这些环节之间有依赖关系有分支判断有需要重试的地方。这不是一次 prompt 能解决的它需要一条流水线。1.1 为什么选 Go 而不是 Python这个选择可能会被很多人质疑。做 AI 相关的东西Python 生态明明更成熟LangChain、LlamaIndex 都是现成的为什么要用 Go 从头搭我的核心考量有三点并发能力。电商场景下单批次常常是几十上百个商品每个商品要走完整个流水线中间涉及多次 HTTP 调用外部模型服务。Python 在这类 IO 密集场景下要么写 asyncio要么上多线程心智负担和维护成本都更高。Go 的 goroutine 让每个商品一个 goroutine、队列控制并发上限这种写法在十分钟内就能搭完而且代码很直白。部署和运维简单。我最终是把服务丢在一台 2C4G 的小机器上跑的Go 编译出来就是单个二进制内存占用低不用配 Python 环境和一堆依赖。这对于小团队和个人项目来说太重要了。流水线的本质是状态流转。一条商品图流水线核心不是调用模型这个动作而是多个步骤之间的状态流转、结果传递、失败重试。Go 在这块的表达能力很强struct channel context 就能把整条链路的骨架写得很清晰。如果你有成熟的 Python 技术栈完全可以用 Python 做同样的事没必要为了技术栈偏好硬换。但如果你本来就写 Go或者后续要考虑并发吞吐和部署成本Go 是完全够用的选择。1.2 整体链路一条流水线的五个阶段流水线跑完一个商品图大致经过这几个阶段图片输入与预处理接收图片做压缩和格式统一调用多模态模型识别商品主体产出结构化的商品描述卖点挖掘与属性抽取基于识别结果提取商品的材质、功能、适用场景、差异化卖点文案模块生成按详情页的固定结构分别生成主图文案、功能模块文案、场景文案、常见问题文案结构化拼装把上面的结果组装成一套统一的 JSON 结构代表完整的详情页内容渲染输出把 JSON 填充到预设的 HTML 模板中产出可直接使用的详情页文件和素材清单这五步对应的就是流水线上的一道道工序。下面我逐个说每道工序是怎么设计和实现的。2. 从商品图到商品属性多模态识别这一步怎么设计流水线的第一道工序是把一张图片变成机器能理解、后续环节能使用的结构化数据。这一步如果做得不扎实后面所有文案生成都是空中楼阁。2.1 图片预处理先压缩再送模型拿到商品图后我做的第一件事不是直接丢给大模型而是先做一次预处理。原因很简单外部模型服务大多按图片大小和分辨率计费或限流一张 5MB 的原始商品图识别结果和压缩后的图差别不大但成本可能差好几倍。我的处理逻辑是用 Go 的image标准库把图片统一压缩到最长边不超过 1024 像素同时转换成 JPEG 格式质量参数设成 85。这样一张图通常在 200KB 左右识别速度和成本都友好很多。func preprocessImage(src io.Reader) ([]byte, error) { img, _, err : image.Decode(src) if err ! nil { return nil, fmt.Errorf(decode image: %w, err) } bounds : img.Bounds() maxSide : 1024 width : bounds.Dx() height : bounds.Dy() if width maxSide || height maxSide { scale : float64(maxSide) / float64(max(width, height)) newWidth : int(float64(width) * scale) newHeight : int(float64(height) * scale) dst : image.NewRGBA(image.Rect(0, 0, newWidth, newHeight)) draw.CatmullRomScaler.Scale(dst, dst.Bounds(), img, bounds, draw.Over, nil) img dst } var buf bytes.Buffer if err : jpeg.Encode(buf, img, jpeg.Options{Quality: 85}); err ! nil { return nil, fmt.Errorf(encode jpeg: %w, err) } return buf.Bytes(), nil }2.2 多模态模型识别用结构化 Prompt 换稳定输出预处理完的图片会交给多模态模型做识别。这里我踩过一个很典型的坑一开始我的 prompt 比较自由让模型描述这张商品图结果模型给出了大段大段的主观修饰什么精致优雅彰显品味这些词看着漂亮但对后续流程来说全是噪音——因为它们无法被结构化解析也没法进入后面的文案环节。后来我把 prompt 改成强结构化的要求明确告诉模型你是一个电商商品分析专家请分析这张商品图严格按照 JSON 格式输出以下字段商品名称、商品品类、主要材质、核心功能、使用场景、目标人群、视觉特征。同时用 response format 参数把所有主流模型服务都开启 JSON 输出模式。这一步的效果立竿见影。模型输出的内容从作文变成了可解析的 JSON后续环节终于能像处理普通数据一样处理模型结果了。Go 这边我用encoding/json做了严格的 Unmarshal只要格式不对直接触发重试。2.3 属性抽取的兜底校验不能完全信任模型结构化的结果出来了但千万别以为可以高枕无忧。我的实测经验是即使开了 JSON 模式模型有时还是会抽风比如某个字段漏了、枚举值不在预设范围内、或者商品品类识别得模棱两可。所以我在识别环节后面加了一层校验逻辑预设了一个合法的品类枚举表模型返回的品类必须命中其中一项否则标记为待人工确认并跳过自动生成关键字段商品名称、核心功能如果为空直接让该条任务进入重试队列最多重试两次用词过滤机制把明显是模型幻觉的夸张形容词如全网第一极致完美在源头上拦截掉这一层兜底看着不起眼但它直接决定了流水线产出的内容能不能用。我的经验是宁可让一条任务标记为人工确认也不要放行一条错误百出的自动生成内容。因为电商文案一旦出现事实错误后果不是内容质量问题而是售后和投诉问题。3. Agent 编排层把大模型调用拆成可控流程而不是一把梭这是整条流水线的核心也是流水线三个字最名副其实的部分。很多刚接触 AI 开发的同学习惯性做法是把所有要求写在一个超长 Prompt 里让模型一口气完成所有事这就是一把梭。你的 Prompt 写个两三千字模型确实能给你一个看起来很完整的详情页文案。但问题是中间任何一个环节出错你无法定位是哪一步出了问题也无法局部重跑。而且模型上下文越长它对结构要求的遵从度越低输出越不可控。我的做法是把流程拆成多个 Agent 节点每个节点只负责一件事节点之间有清晰的前后依赖和数据结构约定。3.1 五个 Agent 节点的职责划分流水线拆成了五个节点每个节点本质上是一个函数输入和输出都是明确的 struct识别节点输入图片输出商品属性和结构化描述卖点节点输入商品属性输出 6 个候选卖点每个卖点包含标题、支持理由、对应图片编号文案节点输入卖点列表输出主图文案、五段功能描述、一段场景文案问答节点输入商品属性输出 4 组常见问题及回答整合节点把所有结果组装成完整 JSON 结构做字段完整性校验触发渲染每个节点内部都调用大模型但节点之间的边界非常清晰。比如文案节点它根本不关心图片是什么样的它只处理上一层卖点节点给它的结构化卖点列表。这样带来的好处是如果某个商品文案质量不行我可以只重跑文案节点而不是从头再来。3.2 Go 里的节点编排实现节点编排我用的是很朴素的实现方式不依赖任何 Agent 框架核心是一个PipelineContext加上一组按顺序执行的函数。type Step func(ctx context.Context, data *PipelineData) error type Pipeline struct { steps []Step } func (p *Pipeline) Add(step Step) *Pipeline { p.steps append(p.steps, step) return p } func (p *Pipeline) Run(ctx context.Context, data *PipelineData) error { for i, step : range p.steps { select { case -ctx.Done(): return fmt.Errorf(pipeline canceled at step %d: %w, i, ctx.Err()) default: } if err : step(ctx, data); err ! nil { return fmt.Errorf(step %d failed: %w, i, err) } } return nil }每个步骤内部都有独立的超时和重试逻辑通过传入的context.Context统一控制链路总超时。主流程把data结构体贯穿传下去每一步在上面做增量更新。最后跑完data.FinalDetail就是整套详情页内容。这种做法其实就是 AI Agent 最朴素的一种落地形态拆解任务、编排流程、调用外部能力大模型、输出结构化结果。不需要引入复杂的 Agent 概念反而更可控。3.3 每个节点的超时与重试策略大模型调用是外部服务不可控因素太多了。我在部署后遇到最典型的问题就是某个模型服务偶尔会响应特别慢拖垮整条流水线让后面排队的商品全部堆积。于是我把每个节点的模型调用都包了一层带超时和重试的 helperfunc callModel(ctx context.Context, prompt string, maxRetries int) (*ModelResponse, error) { var lastErr error for i : 0; i maxRetries; i { reqCtx, cancel : context.WithTimeout(ctx, 30*time.Second) resp, err : modelClient.Generate(reqCtx, prompt) cancel() if err nil { return resp, nil } lastErr err time.Sleep(time.Duration(i1) * time.Second) } return nil, lastErr }实测下来30 秒超时、最多重试 2 次、退避策略是每次递增 1 秒这个组合比较合理。既能容忍偶发的慢响应又不会让单条任务拖住整条流水线。4. 批量商品图的并发与吞吐几十上百个任务怎么不崩流水线单跑一条任务没什么难度真正考验工程能力的是批量场景。我这边最多一次导入过 80 个商品每个商品要跑五个节点每个节点有一次模型调用总共就是 400 次外部 HTTP 请求。如果不做并发控制这一批任务按串行跑好几个小时都出不来。4.1 工作池用 Channel 控制并发上限我实现了一个简单的 worker pool一个带缓冲的 channel 作为任务队列一批固定数量的 worker goroutine 从 channel 里取任务执行。核心参数是并发数我根据目标机器的 CPU 和外部模型服务的速率限制把它设成了 5。func runBatch(ctx context.Context, items []*Item, concurrency int) []Result { taskCh : make(chan *Item, len(items)) results : make([]Result, len(items)) var wg sync.WaitGroup for i : 0; i concurrency; i { wg.Add(1) go func(id int) { defer wg.Done() for item : range taskCh { res : processOneItem(ctx, item) results[item.Index] res } }(i) } for _, item : range items { taskCh - item } close(taskCh) wg.Wait() return results }这里的关键点是用item.Index把结果写回固定位置不依赖 goroutine 的完成顺序。因为每个 worker 处理完的 item 不一定是按输入顺序完成的如果用 append 收集结果最终拿到的东西可能是乱序的。4.2 分批提交防止外部服务限流把 80 个任务一次性全丢进队列5 个 worker 会立刻开始猛跑每个 worker 都在连续请求外部模型服务。有些服务会在短时间内对同一客户端限流而 Go 的 HTTP 客户端默认连接复用会让这个问题发生得更快。我处理的方式是手动分批把 80 个任务分成每批 20 个每批跑完后 sleep 几秒再提交下一批。这个几秒我一开始固定是 5 秒后来调整为根据上一批的成功率动态变化如果失败率超过 10%就把间隔拉大到 15 秒。这种控制策略不需要很精密的算法核心思路就是不要试图在一次批量任务里把并发拉满。拉满的结果往往是不是被限流就是超时报错增加整体吞吐反而更差。4.3 结果落库与断点续跑批量跑完后的结果我全部写入 SQLite 做持久化。每条记录包含商品 ID、流水线版本、每个节点的状态、最终结果 JSON、错误信息。这样做最大的价值不是存储而是给了你断点续跑的能力——某天跑挂了重启程序时先检查数据库已经完成的商品直接跳过只重试失败的商品。SELECT item_id, status, error_msg FROM pipeline_tasks WHERE status failed;这个简单的查询就能让你用一条命令重新处理所有失败任务。对于需要长时间跑批的场景来说这比把所有数据留在内存里、程序一重启就丢要可靠得多。5. 实机踩坑记录最折磨人的几个问题与排查链路这条流水线从第一个能跑的版本到现在稳定运行中间经历了大量让人头秃的排查过程。写几个有代表性的给后来者提个醒。5.1 大模型返回的 JSON 格式不稳定我最初做识别节点时没有强制 JSON 输出模式靠 Prompt 要求返回 JSON。测试时一切良好但上线后大概有 5% 的请求返回了格式错乱的文本。有的多了 Markdown 代码块标记有的在 JSON 外面包了一层解释性文字还有的把字段名拼音化了。这个问题的影响是灾难性的——json.Unmarshal直接报错整条流水线失败。排查链路是先看错误日志确认是 Unmarshal 失败把模型的原始响应打印出来发现格式错乱是偶发的手动重试同一张图成功率和模型服务商的负载有相关性找到根本原因模型在没有强制 JSON 模式时输出格式服从概率分布不是总服从你的 Prompt 要求最终修复所有模型调用全部打开 response format 的 JSON 模式并在 Prompt 末尾再加一句直接输出 JSON不要包含任何其他文字。修复后 JSON 解析失败率降到了 0.5% 以下剩下的偶发情况由重试逻辑兜住。5.2 商品图的背景干扰导致识别结果偏差第二个坑出现在图片质量参差不齐的场景。有的商家提供的商品图是带了复杂背景的模特穿着衣服站在街拍场景里。这种图里商品主体占比可能只有三成模型在识别时容易被背景信息带偏以我为主的约束经常失效输出的品类和颜色可能错到离谱。我的处理方式是在预处理阶段增加一个主体检测步骤用目标检测模型定位商品主体区域然后把图片做裁剪让商品主体在画面中居中且占比不低于 60%再送入多模态模型识别。这一步加完后识别的准确率提升非常明显。实际经验是识别类任务的准确率和输入图片的质量强相关和模型能力的相关性反而不如前者。与其期待模型在糟糕的输入上超常发挥不如把输入质量先提到合格线。5.3 Go 调用外部服务时连接池耗尽还有个隐蔽的问题是在负载稍高时出现的。表现是流水线跑到一半突然集中报错错误信息是连接被拒绝和超时。排查链路是先怀疑外部服务挂掉了但手动用 curl 测试发现外部服务正常。接着又怀疑是 DNS 问题检查后也排除。后来看到 Go 的http.Transport默认配置有个容易被忽视的点MaxIdleConnsPerHost默认是 2MaxConnsPerHost默认是 0不限制。但我真正的问题出现在第二个层面4.1 里的 worker pool 每个 worker 都在并发发起请求每个请求使用的连接都不同DefaultTransport默认空闲连接是复用 2 个以至于在并发场景下每次请求都要新建连接连接建立的时间正好赶上外部服务的速率限制窗口于是集中报错。修复方法是自定义http.Transport把MaxIdleConnsPerHost调到 10同时让所有并发请求共享同一个http.Client而不是每次调用都新建。transport : http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (net.Dialer{ Timeout: 10 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, MaxIdleConns: 100, MaxIdleConnsPerHost: 10, MaxConnsPerHost: 20, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } client : http.Client{Transport: transport}这个细节让我意识到Go 的标准库虽然好用但默认值是为通用场景设计的做高并发调用时必须主动调优。否则表面上程序逻辑没毛病实际吞吐就是上不去。6. 最终效果流水线产出和人工产出的实际对比说了这么多设计最终还是要看效果。这套流水线目前的实测数据是这样单条任务耗时平均 2-4 分钟主要取决于外部模型服务的响应速度批量吞吐5 并发、每批 20 个任务的情况下80 个商品大约 50-60 分钟跑完人工确认比例大约 15% 的任务需要人工介入修改主要是图片识别偏差和文案语气需要微调模板渲染产出直接生成 HTML 详情页头部的主图卖点、中间的功能参数、底部的常见问题一应俱全对比人工制作一个熟练的运营做一套详情页平均要 40 分钟到 1 小时流水线的产出即使加上人工修改的 15 分钟整体效率也提升了 3 倍以上。最关键的是人工做的过程中很容易遗漏模块而流水线的产出结构是固定的每次都会把问答模块、场景模块都补齐不会漏。和直接用 ChatGPT 写文案相比流水线的优势更加明显ChatGPT 只能给你文字而流水线产出的是完整可用的详情页图片素材清单、文案、排版结构、SEO 信息一次性全齐。这就是工具和产品的区别。唯一还没完全解决的是文案的语感和卖点挑选能力。模型写的文案信息密度和结构都没问题但缺少真人操盘手里那点嗅觉——知道什么卖点对该品类的消费者才是最扎心的。合理的方式是先把流水线的主干搭好把生成的内容当草稿在关键位置上保留人工调整的空间。流水线负责效率人工负责临门一脚。关于后续扩展我自己已经在做两件事一是把流水线拆成可复用的组件让它不仅能生成淘宝详情页也能生成小红书笔记、抖音短视频脚本二是把人工修改的部分做成反馈回流让模型在后续生成时更贴近团队偏好。这两件事本质上都是同一个方向——让流水线不只是能跑而是越跑越准。每次看到模型根据我修改过的文案重新生成新内容时那种代码在进化的感觉确实是写普通业务系统给不了的。