
连着加了几天班之后我终于把这条流水线的第一个可用版本跑通了输入一张商品主图输出一个完整的淘宝详情页 HTML连带可选的详情长图。项目是用 Go 写的核心是一条由 5 个 Agent 节点组成的 AI Agent 流水线。这篇文章就把这条流水线拆开讲清楚——每个节点到底在干什么、节点之间怎么串、模型输出不稳定怎么兜底、批量任务里并发和限流怎么处理。这个项目适合三类人看正在做 AI Agent 应用开发的后端工程师想了解“用 Go 调大模型能调出什么花来”的同学以及电商团队里负责工具链搭建的技术同学。如果你还没上过 Agent 项目这篇文章也能给你一个比较完整的工程化视角Agent 流水线不等于堆提示词真正的复杂度都在工程细节里。1. 项目背景与设计思路1.1 一个真实存在的重复劳动电商团队每天要处理十几个甚至几十个 SKU 的商品图。运营拿到一张商品主图之后为了上架要写标题、提炼卖点、做详情页设计。一张图到一套详情页之间隔着的是大量人工环节写文案要找人排版要找人参数说明要从图上一个字一个字抠出来。我当时接到的需求更具体能不能把一张商品图直接变成淘宝详情页最好是批量处理运营丢进去一批图隔一会儿直接收货。传统解法是找外包美工一张详情页报价几十到几百块改稿几轮非常费时间而且不同美工交付的格式还不统一。SaaS 类工具也能用但对图片字段的识别过程不透明生成出来经常是套话模板模板样式想改也改不动。我的诉求很明确自己用代码搭一条流水线让“图片 → 结构化信息 → 文案 → 页面”每一个环节都能独立替换、独立调试这样后续换模型、换模板、换平台都不用重写整套逻辑。1.2 为什么选了 Go 而不是 Python一提到 AI 应用开发很多人下意识觉得该用 Python这没毛病。但这里有一个前提项目跑在电商服务端团队现有技术栈是 Go。这个选择背后有几个比较实在的理由不是跟风也不是炫技。第一部署。Go 编译出来就是一个二进制文件扔到服务器上就能跑跟团队现有微服务的打包发布方式完全一致。如果选 Python等于给整个团队多引入一套 Python 环境和依赖管理方式后续上线、扩容、排障都会多一层负担。第二并发。商品图不是一张一张轮流处理的一次可能是 50 张、100 张。Go 的 goroutine 加 errgroup 处理这种批量并行任务非常顺手信号量做限流也很干净。虽说 Python 也有 asyncio但写起来远没有 Go 这么直白。第三类型约束。Agent 之间传的数据本质上都是结构化 JSON在 Go 里反序列化成 struct 之后后续节点全部用强类型对象操作。大模型万一输出不规范的数据在入口处就会被卡住不会把脏数据一路漏到渲染层。第四模型调用本质就是 HTTP 加 JSON。Go 标准库的 http.Client 完全够用不存在“必须用 Python 才能调模型”的约束。现在的 AI 编码助手对 Go 这种强类型语言的支持也很好写这类工程化代码时确实能提升不少效率。当然Python 生态在大模型 SDK、数据处理方面确实更丰富。如果目标只是快速验证提示词不涉及服务化和批量并发我也会建议先用 Python 脚本把流程跑通。这个项目之所以用 Go是因为它一开始就要走工程化路线而不是验证性脚本。1.3 五个节点组成的流水线架构整条流水线按 Agent 的粒度切分。每个节点只做一件事输入输出统一封装成 Message节点之间通过内存传递数据虽然目前还没上消息队列但这个结构后面接 MQ 也很方便。节点职责输入输出识别 Agent理解商品图提取属性与 OCR 文本图片 URL / 路径ProductInfo JSON规范化 Agent清洗识别结果补全缺失字段ProductInfo标准商品模型文案 Agent基于商品模型生成标题、卖点、规格标准商品模型文案 JSON渲染 Agent套模板生成详情页 HTML 和长图文案 JSONHTML PNG审核 Agent过滤极限词、合规提醒文案 / HTML审核结果这里需要说明一下规范化 Agent 和审核 Agent 在最小实现里可以合并进其他节点但我在项目里坚持把它们独立出来。因为早期测试时脏数据问题太突出了模型多输出一个字段、少输出一个字段、类型跑偏都会在后续暴露。把规范化和审核独立出来相当于给流水线加了数据闸门和合规闸门出了问题能快速定位是哪个环节的锅。Agent 之间的通信格式很简单但很关键。定义最小的 Message 结构和 Agent 接口type Message struct { ID string json:id Source string json:source Data map[string]interface{} json:data } type Agent interface { Name() string Run(context.Context, Message) (Message, error) }为什么用 map[string]interface{} 而不是直接传强类型 struct因为不同节点关注的数据完全不同。识别节点产出图片属性文案节点产出营销文案渲染节点需要的是页面数据。如果直接定义一个大而全的 struct节点之间会耦合得很紧每加一个字段就要动一遍所有节点。用统一的 Message 结构每个 Agent 只读取自己关心的 key写入下游需要的 key每个节点都是独立零件可以单独替换、单独测试。2. Agent 节点拆解与核心实现2.1 识别 Agent让模型替你“看”图识别节点是整个流水线的地基后面的文案质量完全取决于这里的识别结果。早期我尝试过传统 OCR 服务比如阿里云文字识别、百度 OCR它们提取文字很稳定能拿到图上所有文字的坐标和内容。但问题在于详情页文案需要的不只是文字而是语义这是什么商品、什么颜色、什么材质、核心卖点是什么、适合什么人群。传统 OCR 对这些问题完全帮不上忙。所以我最终选择了多模态大模型这条路。把商品图直接扔给支持视觉的对话模型让它输出结构化商品信息。实现上我封装了一个兼容 OpenAI 格式的 LLM Client这样国内主流模型平台都能直接接入。核心代码逻辑大概是type ChatMessage struct { Role string json:role Content string json:content } func (c *LLMClient) VisionComplete( ctx context.Context, system string, prompt string, imageURL string, ) (string, error) { reqBody : map[string]interface{}{ model: c.visionModel, messages: []map[string]interface{}{ {role: system, content: system}, {role: user, content: []map[string]interface{}{ {type: image_url, image_url: map[string]string{url: imageURL}}, {type: text, text: prompt}, }}, }, temperature: 0.1, response_format: map[string]string{type: json_object}, } // 发送 HTTP 请求解析 choices[0].message.content }注意两个细节。一个是 temperature 必须设低图片识别属于事实抽取类任务不需要任何创造性我的项目里固定 0.1 到 0.2。另一个是 response_format 要明确要求 json_object这能避免模型在回答里夹带解释性文字直接给结构化结果。系统提示词也分享出来这是我迭代了很多版才稳定的你是电商图片分析师负责从商品主图中提取结构化信息。 只输出 JSON不要任何解释性文字。 字段要求 name: 商品名称20字以内 sub_category: 商品子类目 brand: 品牌名没有则为 null material: 材质信息没有则为 null colors: 颜色数组最多3个 styles: 风格词数组最多3个 target_users: 适用人群如“男女通用”“女性” key_features: 关键卖点数组最多5条每条不超过15字 ocr_text: 图片上出现的所有文字逐条列出 无法识别的字段必须为 null禁止编造这段提示词的价值在于字段穷举出来了模型不会漏无法识别就填 null避免脑补key_features 限制条数和长度下游文案节点就不用处理超长清单。实际跑下来单张主图大约三到五秒就能返回结构化结果已经接近人工看图了。2.2 文案 Agent把商品信息翻译成营销语言识别出结构化商品信息之后就到了最有意思的环节文案生成。这一阶段要把“蓝牙耳机、黑色、铝合金、ANC 主动降噪、续航 24 小时”这些事实数据变成能打动消费者的商品标题、首屏文案和卖点模块。文案节点是整个流水线里提示词迭代最久的部分核心约束有三条只输出 JSON、控制篇幅、规避极限词。系统提示词最终稳定成这样你是资深电商文案策划服务对象是淘宝店铺运营。 根据用户提供的商品信息生成详情页文案。 严格要求 1. 只输出 JSON键包括 title, slogan, sections, specs 2. title 不超过 30 字要包含品类词和核心卖点 3. slogan 不超过 12 字朗朗上口 4. sections 输出 3-5 个卖点模块每个模块含 heading, body, bullets 5. body 控制在 60 字以内避免空洞修饰词 6. 禁止出现“最”“第一”“国家级”“顶级”“100%”等广告法极限词 7. specs 是键值对数组从商品信息中提取每次请求之后我还会再做一层程序化敏感词检查不单纯依赖模型自觉func checkBannedWords(s string) []string { banned : []string{最, 第一, 国家级, 顶级, 极致, 100%, 绝对, 独一无二} var hits []string for _, w : range banned { if strings.Contains(s, w) { hits append(hits, w) } } return hits }发现命中超限词就记录日志并对该 section 触发一次重新生成。这里有实际经验让模型重新生成整个 section 往往能绕开敏感词但让它在原文上“改掉某个词”效果很差大模型做局部修改经常不听话改着改着就改出了更离谱的表达。而且这条流水线很可能跑一晚生成几百套文案靠人工逐字检查不现实程序化拦截必须前置。2.3 渲染 Agent把文案变成能直接用的页面文案生成完之后数据还是 JSON没法直接上架。渲染节点负责把 JSON 拼成淘宝详情页能用的 HTML 片段。之所以是“片段”而不是完整页面是因为淘宝详情装修后台虽然支持自定义模块粘贴 HTML 源码但会过滤 script、外部链接这些内容。所以生成时有一条硬性规则纯静态结构、内联样式、不依赖任何外部 CSS 和 JS。渲染直接用 Go 标准库的 html/template模板是预置的页面骨架数据从文案节点拿。这里有一个容易被忽略的点html/template 是上下文感知转义的模型输出的文案里如果带了什么特殊标签会被自动转成安全形式不会破坏页面结构。这对自动生成内容来说是额外一层保护。模板大概是这个结构var detailTmpl div stylemax-width:750px;margin:0 auto;background:#fff;padding:16px; h1 stylefont-size:28px;color:#222;{{.Title}}/h1 p stylefont-size:20px;color:#ff5000;margin:12px 0;{{.Slogan}}/p {{range .Sections}} div stylemargin:24px 0;padding:16px;border:1px solid #eee;border-radius:8px; h2 stylefont-size:22px;color:#333;{{.Heading}}/h2 p stylefont-size:16px;color:#666;line-height:1.6;{{.Body}}/p ul stylefont-size:15px;color:#888; {{range .Bullets}}li{{.}}/li{{end}} /ul /div {{end}} table stylewidth:100%;border-collapse:collapse;font-size:14px; {{range .Specs}} tr td stylepadding:8px;border:1px solid #eee;width:120px;{{.Name}}/td td stylepadding:8px;border:1px solid #eee;{{.Value}}/td /tr {{end}} /table /div生成 HTML 之后其实就可以交差了。但电商运营有个习惯他们更愿意直接上传一张长图到详情页。长图怎么生成两种路线一种是用图像库在服务端把模块拼成图片费劲且后续维护成本高另一种是直接用无头浏览器对 HTML 整页截图我用的是 chromedp代码很短func renderScreenshot(ctx context.Context, htmlPath, pngPath string) error { var buf []byte err : chromedp.Run(ctx, chromedp.Navigate(file://htmlPath), chromedp.EmulateViewport(750, 1200), chromedp.FullScreenshot(buf, 100), ) if err ! nil { return err } return os.WriteFile(pngPath, buf, 0644) }这里有一个我踩过的坑FullScreenshot 之前一定要设置 viewport 宽度电商长图一般用 750px 宽度高度会随内容自适应。我第一次跑截图没设宽度结果截出来的图窄得没法看调了半天才发现是视图窗口的问题。3. 实操记录把五个节点串起来跑一遍3.1 工程结构哪些该拆哪些该合项目目录没有花活就是按功能拆分一眼能看懂agent-flow/ ├── cmd/ │ └── agent-flow/main.go ├── internal/ │ ├── agent/ │ │ ├── recognition.go │ │ ├── normalize.go │ │ ├── copywriter.go │ │ └── render.go │ ├── pipeline/ │ │ └── pipeline.go │ ├── llmclient/ │ │ └── client.go │ ├── model/ │ │ └── product.go │ └── config/ │ └── config.go ├── templates/ │ └── detail.html └── go.modmain.go 负责加载配置、初始化 client、组装流水线然后读取图片列表逐个跑。通用校验函数敏感词过滤、JSON 解析兜底、字段类型修复放在 internal/agent 包里。实际工作中有个很现实的要求apiKey、模型名、并发数、超时时间必须全部走配置不能写死在代码里。因为不同模型平台的兼容端点不一样配置文件一改就能切换。type Config struct { APIKey string json:api_key BaseURL string json:base_url VisionModel string json:vision_model TextModel string json:text_model Timeout time.Duration json:timeout Concurrency int json:concurrency }这里没有引入 viper 之类的重配置库项目力求少依赖标准库 encoding/json 读配置文件已经足够。很多 Agent 项目最大的问题不是功能不丰富而是依赖太重重依赖会让后面对接方很痛苦。3.2 编排逻辑和并发控制流水线编排本身是很简单的顺序执行每个 Agent 的 Run 方法返回 Message前一个的输出是后一个的输入。但只有顺序执行那这套东西就只是脚本不是流水线。流水线的价值在批量场景才能真正体现。批量并行我用的 errgroup同时用带缓冲的 channel 做信号量限制并发数避免把模型平台的 QPS 打到限流阈值。这段代码我直接贴出来func runBatch(ctx context.Context, imgs []ImageItem) error { g, gctx : errgroup.WithContext(ctx) sem : make(chan struct{}, cfg.Concurrency) for _, img : range imgs { img : img g.Go(func() error { select { case sem - struct{}{}: defer func() { -sem }() case -gctx.Done(): return gctx.Err() } msg : NewMessage(image, map[string]interface{}{ image_path: img.Path, image_url: img.URL, }) result, err : pipeline.Run(gctx, msg) if err ! nil { return fmt.Errorf(%s: %w, img.ID, err) } return saveResult(img.ID, result) }) } return g.Wait() }errgroup 的好处是任何一个商品处理失败整个批次会收到取消信号正在跑的请求也会通过 context 取消不会傻等一个永远回不来的响应。这个模式用于批量任务极合适某节点连续失败时能快速暴露问题不会出现几十个请求全部卡在重试里的情况。3.3 跑通一单的完整日志第一次完整跑通的时候印象很深刻。我拿一张蓝牙耳机的商品主图做测试输入简化后的运行日志大概是09:00:01 流水线启动 imagebluetooth_earphone_01.jpg 09:00:02 识别 Agent 开始 09:00:07 识别 Agent 返回 类别头戴式蓝牙耳机 颜色[黑色] 材质[铝合金] ocr_text[ANC主动降噪, 24小时续航, 低延迟游戏模式] 09:00:07 规范化 Agent 开始 09:00:08 规范化 Agent 完成 缺失字段补全: brandnull 保留, target_users男女通用 09:00:08 文案 Agent 开始 09:00:13 文案 Agent 返回 title头戴式主动降噪蓝牙耳机 24小时续航 低延迟游戏耳机 slogan静界由我 sections4 09:00:13 敏感词检查 0 命中 09:00:13 渲染 Agent 开始 09:00:15 渲染 Agent 完成 detail_01.html(61KB) detail_01.png(2.3MB) 09:00:15 流水线完成 elapsed14.3s单张图十四秒出一套详情页。开启并发之后五十张图大约五分钟全部跑完。对于电商团队的日常上新速度来说这个效率已经完全够用。当时我做的第一件事是把生成 HTML 打开对照原商品图逐一检查有没有离谱卖点确认没有出现“黑的写成白的”“耳机写成音箱”这类幻觉才开始批量跑。这个抽查习惯我一直保留着后面会细说。4. 常见问题与排查技巧4.1 JSON 输出不稳定解析失败怎么办大模型调用的最大痛点就是输出不稳定哪怕加了 response_formatjson_object偶尔也会出现代码块包裹、前后缀文字、字段类型错乱。我在解析层做了三道防线。第一道防线是剥离。用正则把json ...这类代码块标记去掉再做 JSON 解析。第二道防线是类型修复。模型有时会把 colors 输出成字符串 “黑色” 而不是数组我在反序列化之后会做一次 normalize把类型不对的字段统一修正。第三道防线是字段白名单校验。定义必填字段列表缺失的主动填 null而不是让结构体出现空对象。实现很简单核心是把所有 Agent 的输出都过一遍 normalize 函数。我必须强调永远不要让解析错误暴露到流水线入口外。识别节点和文案节点的输出宁可在节点内部多花两三百毫秒清洗也不要让脏数据泄漏到渲染层更不要让下游节点因为空指针直接崩掉。4.2 限流、超时和重试别堆在一起模型平台接口有 QPS 限制批量任务最容易触发限流。我的 LLM Client 做了两层控制全局信号量限流加指数退避重试。信号量保证同时最多 N 个请求在飞如果收到 429 或 5xx按 1 秒、2 秒、4 秒的间隔重试最多重试三次。这里有个坑容易被忽略重试必须基于新的 context 发起不能一直带着同一个超时 context。否则第一轮已经超时了后续所有重试必然失败因为 context 的截止时间不会重置。另外一个经验是超时时间要按节点分开设置。识别节点请求 40 秒超时文案节点 30 秒超时渲染节点不涉及模型调用二十秒足够。如果所有环节统一用一个十秒超时图片稍微模糊一点或者模型响应慢一点整条流水线就会大面积失败排查起来很打击积极性。4.3 数据缺失、图片信息不足怎么兜底不是每张商品图都信息完整有些图就是纯白底产品图上面没有任何文字。这种图视觉模型也不可能凭空变出品牌和材质。我做了三层兜底。第一层是规范化节点的默认值体系。target_users 默认“男女通用”colors 为空就提取主图主色作为兜底。这些默认值都做成可配置的不同品类可以设置不同策略。第二层是识别置信度标记。模型返回结构化信息的同时增加 confidence 字段confidence 低于阈值的字段在文案节点里直接跳过不强行生成卖点。这个设计很有用模型不确定的品牌名不如下游不生成品牌相关文案。第三层是人工审核出口。每批任务结束后输出一个 summary.json把缺失字段列出来。运营同学可以在批量跑完之后集中补录而不是逐张打开 HTML 检查。第三层很关键。自动流水线的目标不是完全取代人而是把人从重复劳动里解放出来让人只处理异常情况。我一直觉得 AI Agent 类项目必须设计人工介入的口子纯黑盒流水线在真实业务里很难活下来。5. 扩展空间与一点个人体会5.1 这个流水线还能怎么延伸目前的输入是单张主图产出标准化详情页。顺着这条主线可以做的自然扩展其实不少。多图输入是最直接的一个方向把正拍、细节图、场景图都喂给识别节点输出完整卖点库文案节点能调用的素材多很多。批量定时任务也很实用每天凌晨把当天上新商品图丢进流水线早上运营直接收到草稿。再往后可以接上商品发布接口把生成的 HTML 推送到店铺后台草稿箱。渲染层也可以做平台适配。目前模板是针对淘宝的同一套结构化数据完全能生成拼多多、抖音电商的详情样式无非是模板差异和数据口径微调。如果再往 Agent 形态走远一点还能给节点加工具调用能力文案节点卖点不足时能自己触发一次多图分析生成前自动查一下商品类目的行业规范这就是更完整的 Agent 形态了。项目代码在这方面留了接口但目前没必要把复杂度加上去。5.2 踩坑踩出来的几个结论第一个结论Agent 流水线的复杂度不该花在“怎么让 Agent 更聪明”上而是花在“怎么让 Agent 的输出更可预测”上。模型本身的聪明程度已经够用我大部分时间处理的都是解析、校验、兜底和重试这些工程问题而不是模型能力问题。第二个结论Go 在 AI 应用编排这个位置上被低估了。类型系统、context 取消机制、并发原语天然适合做多节点流水线。如果你已经会 Go完全没必要为了 AI 项目把技术栈换成 Python。模型调用无非是 HTTP 加 JSON真正的复杂度在工程组织上。第三个结论不要迷信“一条龙生成”真实业务永远需要人看一眼。哪怕流水线已经稳定跑出几百套详情页我依然会在每批任务结束后抽查几单检查卖点有没有夸张、广告词有没有踩线。自动化和人工审核不是对立关系而是配合关系。一个连审核入口都没有的自动化系统在电商这种重视合规的场景里迟早出事。最后分享一个小技巧模型协议里那套 system prompt是整个项目里迭代次数最多的文件。每次跑完发现某类商品识别不准或者文案格式不对先不要怀疑代码回去翻模型返回的原始 JSON逐字段对着看。哪里不对就改提示词而不是在代码里打补丁藏问题。提示词稳定下来之后代码几乎不用动。这个习惯帮我省掉了大量无效调试时间也是我从这个项目里收获最大的经验。