ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Go构建AI Agent流水线:从商品主图自动生成电商详情页

用Go构建AI Agent流水线:从商品主图自动生成电商详情页 大概半年前接了一个电商服务商的项目需求很简单运营每天要上几千个新SKU每个商品详情页都得人工写——主图提炼卖点、写标题、凑详情文案、整理规格参数一个详情页快的也要三四十分钟赶上大促根本写不过来。甲方的原话是能不能丢一张主图进去自动把整套详情页给我吐出来我第一反应是用 Python 写毕竟 AI 相关的 SDK、框架基本都是 Python 生态。但真正坐下来盘需求的时候发现这东西要作为 HTTP 服务被内部系统和外部商家反复调用要同时处理批量商品图要扛住早晚高峰的并发请求还要能在服务器上像普通后端服务一样部署、监控、重启。用 Python 写一套带并发控制、限流、熔断的服务不是不行但折腾一圈下来部署体积、内存占用、并发调度都要操不少心。于是我把目光转回到了 Go 上——最终用 Go 搭了一条 AI Agent 流水线从一张商品主图到一整套淘宝详情页全程自动化。这篇就把整条流水线的设计思路、每个 Agent 节点的实现细节、并发调度方案以及我在实际运行中踩过的坑完整记录下来。不管你是准备用 Go 做 AI 应用还是想了解 Agent 流水线怎么从 0 到 1 落地这篇都值得花几分钟看完。1. 为什么用 Go 搭 AI Agent 流水线而不是 Python 或现成框架先说结论语言选型从来不是哪个更 AI而是哪个能把 AI 能力稳定地包进业务系统里。Go 在 Agent 场景里的优势我在这个项目里体会得很实在。1.1 这个项目本质上是一个带 AI 环节的后端服务很多人在搭建 AI Agent 的时候容易陷入一个误区把 Agent 当成一个 Python 脚本在跑。但实际上像一张商品图生成一整套详情页这种需求落到工程层面就是一个标准的后端服务外部请求进来带着图片 URL 或 base64 数据服务内部要编排多个 AI 调用——图像理解、标题生成、卖点提取、文案扩写、合规检查每个环节可能调用不同的模型有的环节还要查规则库比如违禁词最后把结果拼装成结构化数据返回。这套东西要跑得好真正考验的是并发调度、链路稳定性、超时控制、失败重试、资源占用。而这些恰好是 Go 最擅长的事。goroutine 起几千个都没压力channel 做节点间的数据流转又天然清晰部署出来就是一个静态二进制没有 Python 那套虚拟环境、依赖安装、GIL 的破事。1.2 为什么不直接用 LangChain / Dify / Coze这里我要泼一盆冷水如果你的目标是快速验证一个 Agent 想法用 Dify、Coze 这类平台完全没问题拖拽一下就能跑通Dify 的知识库流水线也确实做得越来越成熟。但如果你是要做一个面向业务系统的、需要扛并发的 Agent 服务现成框架反而会变成束缚。我用过 LangChain 的 Python 版也研究过 Dify 的知识库流水线工作流最大的感受是框架为了兼容各种场景抽象层叠得太厚。你要做图片理解 - 提取属性 - 生成文案 - 合规校验这种强业务逻辑的流水线框架给的 Agent 概念反而是多余的。我更想要的是每个处理环节是一个独立的函数或接口环节之间的数据流转用强类型结构体而不是 JSON 字符串传来传去并发控制、限流、熔断这些直接用 Go 的 x/time/rate、gobreaker 这些库解决整个流水线能像普通后端接口一样加监控、打日志。与其在框架的抽象里绕来绕去不如直接用 Go 把流水线写出来每个节点自己控制出问题能精准定位到是哪个环节、哪次调用。1.3 我在选型时的对比表我把自己当时对比的几个方案整理了一下供参考方案优势劣势适合场景Python LangChain生态全、上手快、资料多并发弱、部署重、抽象深快速原型、数据分析、内部脚本Dify / Coze 等平台零代码、自带知识库和工具私有化部署受限、定制能力弱、难扛高并发运营搭工作流、非技术团队Go 自建流水线并发强、部署轻、稳定性高需要自己写编排、AI SDK 选择少一些面向业务系统、需要长期维护、要扛流量当然Go 生态里也有现成的 Agent 框架比如 spring-ai-agent 那个思路在 Java 圈子多一些Go 这边类似 Spring AI 的成熟方案还比较少所以自建是现阶段比较合理的路径。2. 流水线整体设计从一张主图到一整套详情页的任务拆解一条 Agent 流水线能不能干活关键不在单个模型有多聪明而在于你把这件大事拆成了几个什么节点每个节点之间怎么协作。这一步我花的时间比写代码多得多。2.1 先把淘宝详情页拆成可生成的模块要生成一整套详情页不能指望一个 Agent 一口吃成胖子。我先把详情页拆成了这些固定模块商品标题30~60字包含核心关键词、规格、卖点要符合淘宝标题规范卖点标签3~5条短句每条不超过12个字比如纯棉透气一机两用详情描述3~6个段落每个段落配一个小标题讲清楚一个卖点规格参数从图片中识别或推断如材质、尺寸、颜色、重量等常见问题FAQ3~5条针对商品可能的疑问生成问答注意事项使用、保养、售后相关的声明。每个模块生成逻辑不同有的依赖图片理解结果有的纯靠文案能力有的要过规则校验。把它们拆开每个节点都可以单独测试、单独调优、单独替换模型这是流水线设计最重要的一步。2.2 流水线的节点编排与数据流转我的流水线是线性结构带一个并行分支整体是这样走的图像理解节点输入商品主图输出商品的基础描述、可见属性、场景要素属性规整节点把图像理解的结果对照行业属性模板补全缺失字段、统一枚举值标题生成节点基于规整后的属性生成多个候选标题并打分卖点提取节点从属性和场景中提取卖点标签详情文案节点基于以上所有结果分段生成详情描述和FAQ合规校验节点跑违禁词表 模型二次判断结构化输出节点组装最终 JSON。第5步的详情文案其实是个并行的子流水线可以针对每个卖点段落单独开 goroutine 生成然后再合并。这个在后面并发部分详细讲。2.3 中间产物的数据结构设计流水线节点之间传递的不是字符串而是强类型的 Go struct。这是 Go 做流水线最舒服的地方——你在编译期就能发现字段对不上的问题而不是等到运行时解析 JSON 才发现 key 写错了。我定义了这样的核心数据结构// ProductInfo 是图像理解节点输出的商品基础信息 type ProductInfo struct { SchemaVersion string json:schema_version Category string json:category Title string json:title Attributes map[string]string json:attributes Scenes []string json:scenes Materials []string json:materials Colors []string json:colors Uncertainties []string json:uncertainties } // SellingPoint 是卖点标签 type SellingPoint struct { Tag string json:tag Evidence string json:evidence Confidence float64 json:confidence } // DetailSection 是详情描述中的一个段落 type DetailSection struct { Heading string json:heading Content string json:content Keywords []string json:keywords } // FinalDetail 是最终输出的详情页结构 type FinalDetail struct { Title string json:title SellingPoints []SellingPoint json:selling_points Sections []DetailSection json:sections Specs map[string]string json:specs FAQ []FAQItem json:faq Notices []string json:notices }这里的 SchemaVersion 字段是我后来加上去的刚开始没这个结果 prompt 迭代几次之后旧缓存数据跟新代码解析不兼容排查了半天。加个版本号是最省事的做法。2.4 状态管理与失败重试策略线性流水线的好处是简单坏处是中间一旦有个节点挂了整条链路就得重来。所以我的策略是不在节点外部做整体重试而是在节点内部做局部重试。每个节点我封装成一个 Step 接口type Step interface { Name() string Run(ctx context.Context, input map[string]any) (map[string]any, error) RetryPolicy() RetryPolicy }统一的管线执行器会检查每个节点的重试策略默认重试3次首次间隔1秒之后指数退避。图像理解这类成本比较高的节点重试次数压到2次合规校验这种本地规则为主的节点不重试直接失败。这样设计的目的很明确如果文案生成节点因为模型超时挂了重跑这一个节点只要几秒钟如果整体重跑图像理解也要再花十几秒成本浪费不说还可能触发上游 API 的限流。3. 核心 Agent 节点的实现细节与 Prompt 设计节点框架搭好了真正的活儿是让每个 Agent 节点输出稳定、可解析的结果。这一章我挑几个关键节点把 Prompt 和实现逻辑拆开讲。3.1 图像理解节点让多模态模型当眼睛这个节点是整个流水线的基石后续所有内容都建立在它输出的商品描述和属性之上。我要求模型输出的必须是一个 JSON包含商品类目、可见特性、场景、材质、颜色以及模型的不确定项——这个关键。Prompt 的骨架大致是这样的你是电商商品信息提取专家。请分析这张商品主图输出 JSON 格式的商品信息。 要求 1. 只描述图中可见的内容不要推测不可见信息 2. 如果某个属性在图中无法确认放入 uncertainties 字段不要编造 3. attributes 中的 key 限定使用以下枚举材质, 颜色, 尺寸, 重量, 风格, 功能, 适用场景 4. 商品标题建议控制在 30 字以内基于图中信息生成。 5. 直接输出 JSON不要包含任何解释性文字。这里最重要的一条是第2条不确定项。为什么因为多模态模型在图片信息不足时会把猜测包装成事实输出。比如一个保温杯的主图如果是纯色背景模型可能会脑补出容量500ml但图上根本没有这个信息。让它明确输出 uncertainty我才能在后续节点决定是补问用户还是删掉这个属性。调用方面Go 这边我用的是 OpenAI-compatible 的接口SDK 用 github.com/sashabaranov/go-openai这个库在 Go 社区里维护得不错支持多模态输入func (s *ImageUnderstandingStep) Run(ctx context.Context, input map[string]any) (map[string]any, error) { imageURL : input[image_url].(string) resp, err : s.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ Model: gpt-4o-mini, // 根据预算和效果选 Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleUser, Content: []openai.ChatMessagePart{ {Type: openai.ChatMessagePartTypeText, Text: s.prompt}, {Type: openai.ChatMessagePartTypeImageURL, ImageURL: openai.ChatMessageImageURL{URL: imageURL}}, }, }, }, ResponseFormat: openai.ChatCompletionResponseFormat{Type: json_object}, }) if err ! nil { return nil, err } return parseImageResult(resp.Choices[0].Message.Content) }用 ResponseFormat 强制 JSON 输出这个非常重要后面会再讲。3.2 标题生成节点先发散再收敛而不是一次生成淘宝标题有严格的字数限制和关键词布局要求。我一开始是让模型直接生成一个标题效果很差——要不就是不够30字要不就是把卖点堆砌得毫无逻辑。后来改成先发散、再收敛的两段式第一轮 prompt输入规整后的属性要求生成8个候选标题每个都必须包含核心属性词第二轮 prompt把8个候选标题和用户搜索热度词列表一起给模型让它按可读性、SEO覆盖、无违禁词打分选出一个最优。这样虽然多了一次模型调用但效果稳定得多。因为第一轮模型有发散空间不会挤牙膏第二轮有了对比评价标准也清晰不需要一次性就给出完美答案。这里应用的另一个经验是每个标题候选都要让模型标注它覆盖了哪些属性词。后续的合规校验节点会检查这些属性词是否正确出现在标题里防止模型发挥过头把商品属性改了。3.3 合规校验节点规则引擎和模型双重把关淘宝详情页不能出现违禁词、极限词最、第一、顶级这种这是上线前的硬门槛。这个节点我做了两层第一层是本地规则库。我把常见的违禁词、广告法极限词整理成一个词表加载到内存里跑一遍字符串匹配。这层速度快、零成本、不需要调模型。第二层是模型判断。因为有些违禁词是语义级的比如全网销量第一这种字面上不在词表里但语义违规。我让模型针对标题和详情文案做一次合规审查输出违规项列表。这层不用独立大模型直接复用一个便宜的 fast 模型就行。值得提醒的是合规校验节点不应该只返回通过/不通过而是要返回具体的违规词和修改建议。这样流水线可以自动触发一次修订把违规文案塞回文案生成节点附上违规原因让它重写。这个审查-修订闭环是把人工审核成本降下来的关键。3.4 结构化输出的稳定性function calling 比 prompt 硬编码可靠凡是要求模型输出 JSON 的节点我都走 function calling / structured output而不只是用文字 prompt 说输出 JSON。原因很简单模型用自然语言生成 JSON 时偶尔会在 JSON 外面包一层 markdown 代码块或者把 key 从 snake_case 飘成 camelCase或者干脆在 JSON 后面多一句希望这个结果对你有帮助。用 function calling 的方式模型的输出会被强制限定在你定义的工具参数结构里tools : []openai.Tool{ { Type: openai.ToolTypeFunction, Function: openai.FunctionDefinition{ Name: store_product_detail, Parameters: map[string]any{ type: object, properties: map[string]any{ title: map[string]any{type: string}, sections: map[string]any{ type: array, items: map[string]any{ type: object, properties: map[string]any{ heading: map[string]any{type: string}, content: map[string]any{type: string}, }, }, }, }, required: []string{title, sections}, }, }, }, }output 结构交给模型强制生成之后我的代码只需要做一次 JSON 解析基本不会出现格式漂移。即使个别调用飘了也会落入重试策略不会直接污染流水线。4. Go 在并发调度上的实战一条流水线如何扛住批量商品图串行一条流水线跑完一张商品图大概要 30~60 秒看模型响应速度。如果商家一次性上传 500 张图串行得跑六七个小时这谁能忍。Go 在这个环节的价值真正体现出来了。4.1 任务级并发每个商品一条流水线互不干扰我的方案是外层用 worker pool每个商品图作为独立任务提交到 channelworker goroutine 从 channel 里取任务各跑各的完整流水线。Go 的 goroutine 这里非常省心500 个商品并发跑也不会把服务打挂。worker pool 的核心代码大概是这样的func (p *Pipeline) RunBatch(ctx context.Context, images []string, workers int) []Result { taskCh : make(chan string, len(images)) resultCh : make(chan Result, len(images)) var wg sync.WaitGroup // 启动 workers for i : 0; i workers; i { wg.Add(1) go func() { defer wg.Done() for imageURL : range taskCh { detail, err : p.RunSingle(ctx, imageURL) resultCh - Result{URL: imageURL, Detail: detail, Err: err} } }() } // 分发任务 for _, img : range images { taskCh - img } close(taskCh) wg.Wait() close(resultCh) // 汇总 var results []Result for r : range resultCh { results append(results, r) } return results }这里有个细节resultCh 的 buffer 要开足够大否则某个 worker 处理慢其他 worker 的结果会被阻塞。我这里直接开了和任务数一样大的 buffer防止任何 worker 卡在发结果上。4.2 并发数不是越高越好限流、成本与延迟的平衡很多人一上来就开 200 个 goroutine 并发调模型结果模型 API 直接返回 429然后重试风暴服务更慢了。这里的关键是要搞清楚瓶颈在哪。我统计过自己项目的耗时分布图像理解节点约 4~8 秒多模态模型较慢标题生成节点约 2~3 秒卖点提取节点约 2 秒详情文案节点3 个段落并行每个约 3~4 秒合规校验节点约 1~2 秒串行总耗时约 15~18 秒开并发后详情生成部分缩到约 8~10 秒模型 API 的限流是硬约束。以 OpenAI 为例有 RPM每分钟请求数和 TPM每分钟 token 数两个维度。我不可能无限开并发得先算清楚账每天要处理 2000 个商品工作时间 8 小时每商品约 15 次模型调用 每分钟调用约2000 × 15 / (8 × 60) ≈ 62.5 次请求/分钟这是平均情况下的需求。加上早晚高峰的波动我把目标定在 120 RPM 的调用能力然后按这个值设置本地限流和 worker 数量而不是盲目把并发拉到很高。Go 里做本地限流我用的 golang.org/x/time/rate 的令牌桶。每个模型调用环节共享一个限流器防止某个环节突发请求把所有配额吃掉var llmLimiter rate.NewLimiter(rate.Limit(120), 1) func callLLMWithLimit(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) { if err : llmLimiter.Wait(ctx); err ! nil { return openai.ChatCompletionResponse{}, fmt.Errorf(rate limited: %w, err) } return client.CreateChatCompletion(ctx, req) }4.3 超时、重试与熔断不让一个慢接口拖垮整条链路AI 模型调用是不稳定的偶尔响应要 30 秒偶尔直接超时。流水线里每个环节都必须有独立超时控制。我用 context.WithTimeout 给每个节点一个单独的 deadline保证某个节点超时不影响整个 worker 的回收。熔断我也加了。如果一个小时内某个模型接口连续失败超过 20 次我会让对应的节点快速失败不再继续打这个接口等冷却期过了再恢复。Go 这边直接用 gobreaker 这个库配置非常简单cb : gobreaker.NewCircuitBreaker[any](gobreaker.Settings{ Name: image-understanding, MaxRequests: 5, Interval: 60 * time.Second, Timeout: 30 * time.Second, ReadyToTrip: func(counts gobreaker.Counts) bool { return counts.ConsecutiveFailures 10 }, })熔断的目的是保护——当上游模型服务已经开始出问题的时候你继续拼命打它的接口只会让情况更糟。快速失败反而能让用户体验更可控比如直接返回生成失败请稍后重试而不是一直转圈。4.4 并发效果的实测数据直接上我压测的一组数据供参考。机器是 4 核 8G 的云服务器模型用 GPT-4o-mini 和 Qwen-VL-Max 混合并发 worker 数处理 500 张商品图总耗时CPU 占用峰值内存占用峰值失败率528 分钟32%420 MB0.4%1014 分钟61%780 MB0.8%208 分钟88%1.4 GB1.6%可以看到并发从 10 翻到 20耗时只减少了不到一半CPU 和内存倒是涨了不少失败率也在上升。原因很直接模型 API 的响应速度是瓶颈本地并发上去了但上游的排队时间也变长了。所以我最终在生产环境把 worker 数定在 10~12 之间兼顾吞吐和稳定性。5. 踩坑实录AI 输出不稳定时怎么保证流水线不中断这条流水线上生产之后真正让我花时间处理的不是框架代码而是模型输出的各种幺蛾子。这些坑我一个个记录一下。5.1 多模态模型看不懂图片里的文字细节有一次测试商品主图上印着500ml 保温杯的标签但模型把 500ml 识别成了 500g。这个错误在后面生成文案的时候被放大成杯身仅重500g式的胡话。排查了很久发现不是模型不行是我 prompt 里没要求它识别图中出现的所有文字并原文引用。修复方法在图像理解节点的 prompt 里加了一条如果图中出现文字或数字请原样提取并放至 attributes 中如果存在相互矛盾的信息保留所有可能值并标注。从那之后图片文字识别错误率降了一个数量级。5.2 生成的标题总是超过淘宝的 60 字限制淘宝主标题最长 60 个字符模型默认的中文分词下经常生成 70~80 字的长标题。一开始我靠 prompt 约束后来发现约束不稳定——prompt 告诉它 60 字它偶尔还是给你冒个 65 字的。最终解决办法不在模型层硬卡而是在代码层做后处理。标题生成后用 rune 统计字数超过 60 字就按核心属性词优先的策略截断截断后如果末尾断在奇怪的地方再让模型重写一次收尾。代码层兜底永远比 prompt 约束可靠。5.3 上下文太长导致详情文案越写越乱详情文案节点需要把图像理解结果、卖点标签、属性规整结果全部塞进 prompt 里。如果一个商品属性特别多比如电子产品prompt 可能到 4000 token模型会出现回廊效应——前面还记得要让文案围绕卖点后面就开始自由发挥。这里的解法是拆段落。详情文案节点不再一个 prompt 生成全部段落而是每个卖点单独生成一个段落然后并行跑最后合并。每个段落的 prompt 只包含一个卖点、对应属性和上下文摘要token 控制在 800 以内。效果立竿见影段落之间的风格差异也小多了。5.4 模型返回的 JSON 格式漂移解析失败的兜底方案即使用了 function calling偶尔也会碰到模型返回空 content 或者结构不符合预期的情况。我做了三层兜底直接 JSON 解析失败时尝试正则提取json ...代码块提取失败时用gpt-4o-mini 修复 JSON的轻量调用修复一次连续修复失败则标记该节点失败走重试策略。这三层下来解析失败导致的流水线中断率从百分之三降到了万分之几。这个兜底方案比较通用建议做 Agent 应用的朋友都预留一层。5.5 成本控制缓存 分模型把单详情页成本降了一半AI 流水线的成本大头是模型调用。我做了三个优化结果缓存对图像理解节点做哈希缓存输入图片 URL 不变就复用结果同一个主图反复生成时不重复调用多模态模型分级用模型图像理解用贵的模型保证准确率标题/卖点用中端模型合规校验用最便宜的 fast 模型批量合并小模型的调用走 batch API如果服务商支持不但省成本还能规避 RPM 限制。优化之后单商品详情页的模型成本从原来的约 0.8 元降到了 0.35 元左右每天处理 2000 个商品一天能省下近千元的调用费。这个数字对中小商家来说是实打实的利润。6. 效果验证与后续扩展的思考流水线跑通并稳定运行之后我做了几轮效果验证。随便挑一个结果举例子输入是一个保温杯的主图输出标题为316不锈钢保温杯男女士大容量便携车载水杯 简约办公杯泡茶杯卖点标签是316不锈钢24小时保温车载适用大容量一键弹盖详情文案分了五个段落FAQ 生成了保温时长、能否装碳酸饮料、清洗方式、容量规格、快递发货时间五条基本覆盖了这个品类最常见的咨询点。现在整个流水线从一张主图提交到详情页 JSON 返回单个商品平均耗时约 8 秒相比人工的 30~40 分钟效率提升了上百倍。人工要做的事只剩抽检和个别修订运营团队可以腾出精力去优化主图和转化率而不是跟键盘搏斗。这个项目做完之后我在想还能往哪个方向扩展。我自己比较看好的方向是把 Dify 知识库流水线的思路融合进来——把品牌手册、历史爆款文案、行业规范塞进知识库商品图进来之后先检索相关知识片段再带着这些片段去生成文案。这样生成的内容就不是通用电商文案而是你家店铺风格的文案差异化会非常明显。另一个扩展方向是把这个流水线抽象成通用的文档生成管道不仅服务淘宝详情页还能扩展到小红书笔记、京东详情页、拼多多商品文案。每种平台只需要替换规则节点和输出模板中间的图像理解、属性规整、卖点提取这些节点完全是复用的。我在实际操作中最深的一个体会是流水线这个东西上游一个小问题的输出会在下游被不断放大。图像理解节点多一个不确定信息文案生成节点就会多一次胡编乱造。所以设计 Agent 流水线时宁可让每个节点谨慎一点、多输出一点不确定性标记也不要让节点悄悄把错误吞掉。最后再分享一个小技巧给每个 Agent 节点的中间产物加上 schema 版本号你在迭代 prompt 的时候历史数据才能继续被代码正确解析这个细节在流水线项目里价值极大。
返回列表