上周我把运营部的活儿给抢了。扔给这条流水线一张商品图三分多钟后它吐出来一整套淘宝详情页的 HTML主图、卖点、参数表、场景描述、售后说明全都排好了版。这条流水线是用 Go 写的核心是一个多阶段的 AI Agent从图片理解、文案生成到页面渲染全部串起来。我把完整的设计思路、关键代码和踩过的坑整理出来给想用 Go 做 AI 应用、或者想自己搭 AI Agent 流水线的同学一个参考。先说我解决了什么问题以前做一条详情页运营要先找设计出图、自己写文案、再找美编排版单条链接走完流程大概要半天。现在输入一张商品图系统自动识别商品类目和属性生成标题、卖点、参数说明再渲染成详情页 HTML运营只需要做微调。整个过程的核心不是某个单独的 AI 能力而是怎么把图像识别、文本生成、模板渲染这些环节串成一条可控、可维护、可并发的流水线。Go 在这里不是配角它承担了编排、并发、容错和渲染的几乎全部工作。这篇文章我尽量讲透三层第一层是整体架构为什么流水线比单 Agent 更靠谱第二层是每个环节的具体实现包括图像预处理、多模态模型调用、结构化输出强约束、模板渲染第三层是并发调度和实战问题排查。代码都是可以直接抄走的片段但更重要的是背后的取舍逻辑。1. 需求拆解这条流水线到底要干几件事1.1 从一张商品图到详情页中间隔着多少工序我一开始把这件事想简单了以为直接让大模型看一眼图片让它输出 HTML 就完事。实测走了两步就发现完全不行第一模型直接输出的 HTML 基本没办法直接商用不是 div 嵌套混乱就是内联样式堆成一坨更别提 id 和 class 命名完全不可控第二淘宝详情页是有固定信息结构的买家要看什么、平台规则要求什么这些都是隐性的业务约束。真正把需求拆开之后详情页可以被拆成这几个独立环节商品主体识别这是什么、大概什么类目、属性补全材质、尺寸、适用场景、卖点提炼这款商品凭什么值得买、文案生成标题、五段式描述、参数表文案、结构编排各个模块按什么顺序呈现、HTML 渲染和图片资源落地。任何一个环节如果让大模型自由发挥输出都会失控。所以我的结论是这条流水线必须是一个有固定骨架的 Agent 多阶段流程而不是一个 prompt 打通关。1.2 为什么是 Go不是 Python这个项目如果放在两年前我大概率会用 Python 写因为 AI 生态在那里LangChain 等等框架都是 Python 优先。但这次我坚定选 Go原因有几个。第一我们团队的服务端主要技术栈是 Go这套流水线最终要接进现有的商品管理系统用 Go 可以少维护一套服务。第二Go 部署实在太省事编译成单个二进制直接丢服务器跑不用装 Python 环境和一堆依赖。第三流水线是典型的高 IO、多并发的场景多个环节之间要并行处理Go 的 goroutine 和 channel 写并发比 Python 舒服太多。第四大模型调用本质上就是 HTTP 加 JSONGo 标准库对这两件事支持得非常好。有人会问Python 生态里的 AI Agent 框架更强比如 LangChain。但我实际用下来的感受是这类框架抽象层级太高调试的时候要穿透好几层封装出了问题很难定位。自己用 Go 写一条流水线每个环节就是一个接口调用关系一目了然出了问题管道里查日志就行。对于我这种需要深度控制每个环节的开发者来说裸写比框架更顺手。1.3 整体架构一条线穿三个站整个流水线的演进路线可以理解为三个站点每个站点又是一个独立的小 Agent站点 A 是理解站输入商品图调用多模态模型识别商品主体、类目、颜色、材质等基础信息再用本地的类目知识库做属性补全和校验站点 B 是生成站基于站点 A 的结构化信息分多个子任务生成详情页文案包括商品标题、卖点列表、参数描述、场景故事、售后说明站点 C 是渲染站把生成的文案和图片资源填充到模块化模板里输出完整的 HTML并落地所有素材文件。这三个站点之间通过一个统一的数据结构传递结果每个站点的输出都是 JSON这样做的好处是每个环节可以独立测试、独立替换。比如以后想换更强的多模态模型只需要改站点 A 的 adapter后面的生产站和渲染站完全不用动。2. 核心环节一让 Agent 真正看懂商品图2.1 图像预处理与主图信息提取商品图直接丢给多模态模型是能识别的但结果不稳定。一张 3000x3000 像素的原始大图模型处理起来慢而且图片里的噪点、背景杂物会干扰识别。所以我在进入模型之前加了一个图像预处理步骤。预处理做三件事第一校验图片格式和尺寸只接受 JPEG 和 PNG超过 10MB 的先压缩第二用 Go 的 image 库把图片等比缩放到模型适配的尺寸我这边统一压到 1024 宽度长边不超过 1536第三提取基础元信息包括主色调、是否存在白底、图片中的文字区域数量这些信息后续会作为 prompt 的辅助上下文。func PreprocessImage(path string, maxWidth int) (*PreprocessedImage, error) { f, err : os.Open(path) if err ! nil { return nil, err } defer f.Close() src, _, err : image.Decode(f) if err ! nil { return nil, fmt.Errorf(decode image: %w, err) } bounds : src.Bounds() width : bounds.Dx() height : bounds.Dy() if width maxWidth { scale : float64(maxWidth) / float64(width) newHeight : int(float64(height) * scale) dst : image.NewRGBA(image.Rect(0, 0, maxWidth, newHeight)) // 这里用 draw.NearestNeighbor 做缩放速度快细节损失可接受 draw.NearestNeighbor.Scale(dst, dst.Bounds(), src, bounds, draw.Over, nil) return PreprocessedImage{ Image: dst, Width: maxWidth, Height: newHeight, }, nil } return PreprocessedImage{ Image: src, Width: width, Height: height, }, nil }2.2 接入多模态模型的结构化输出预处理完成后把图片转成 Base64 或上传到临时图床拿到 URL然后构造多模态模型的请求。现在主流的多模态模型基本都支持 OpenAI 兼容的接口格式我用 Go 直接发 HTTP 请求就可以不需要引特别重的大模型 SDK。这里最关键的一点是必须强制模型输出 JSON 而不是自然语言。我会在 system prompt 里明确规定输出结构同时在 user prompt 里再次强调“只输出 JSON不要任何解释”。即便如此模型偶尔还是会输出带 markdown 代码块标记的内容所以我在解析层做了兜底用正则把json 和剥掉再解析。type VisionResponse struct { Category string json:category Name string json:name Colors []string json:colors Material string json:material Scene string json:scene VisualHints []string json:visual_hints } func AnalyzeProductImage(ctx context.Context, client *Client, imageURL string) (*VisionResponse, error) { prompt : fmt.Sprintf(请分析这张商品图输出 JSON 格式的识别结果字段包括 category(商品类目), name(商品名称), colors(主要颜色列表), material(材质不确定就留空), scene(使用场景), visual_hints(图片中可见的卖点线索列出3-5条)。 只输出 JSON不要输出任何解释。图片地址%s, imageURL) resp, err : client.ChatCompletion(ctx, ChatRequest{ Model: vision-model, Messages: []Message{ {Role: system, Content: 你是电商商品分析师必须输出严格 JSON。}, {Role: user, Content: prompt}, }, ResponseFormat: json_object, }) if err ! nil { return nil, err } clean : stripCodeFence(resp.Content) var v VisionResponse if err : json.Unmarshal([]byte(clean), v); err ! nil { return nil, fmt.Errorf(vision response parse error: %w, err) } return v, nil }2.3 属性补全知识库才是兜底多模态模型能识别出商品的大致类目但细节属性经常漏。比如一件连衣裙模型能识别出“连衣裙”和颜色但材质、领型、袖长这些属性经常说错或不说。这时候光靠模型不够我在本地维护了一个类目属性知识库用 JSON 文件存储结构大概是每个类目对应一组必填属性和候选值。拿到模型返回的类目之后我会先查知识库找到这个类目下应该补充哪些属性然后拼一个第二轮的补全 prompt把模型第一轮的识别结果和需要补全的属性清单一起发过去。这里有个小技巧为了控制 token 消耗我不用让模型重新生成全部信息只让模型针对缺失字段做填充。比如识别到“连衣裙”类目知识库会给出必填属性版型、裙长、腰型、适用季节。我就把“材质识别为棉适用季节缺失”这样的上下文丢给模型让模型只补缺的字段。这样一轮补全之后属性完整率能从 60% 提升到 90% 以上。2.4 这里实际踩过的坑第一版我以为一次识别就够了结果很多属性被模型随口胡编。比如模型把一条聚酯纤维的裙子识别成“丝绸”这种错误后面所有文案都会跟着错。后来我在系统 prompt 里加了一句“不确定的属性必须输出 unknown不要猜测”并加了一轮人工校验机制如果关键属性置信度低于阈值就进入人工补录队列。第二坑是图片压缩太狠导致识别错误。压到 512 宽度时模型把深蓝色识别成黑色把皮质的纹理识别成光滑。现在我的策略是压缩宽度不低于 1024识别类目用压缩图但如果商品有文字细节图就原图走一次单独识别。3. 核心环节二文案生成的分段流水线3.1 把详情页文案拆成五个子任务详情页文案不应该是一大段生成拆成五个子任务分别生成效果会好很多也更可控商品标题、五段式卖点、参数说明、场景描述、售后说明。每个子任务使用不同的 prompt 模板对输出的长度、语气体例都有独立的约束。为什么要拆我试过一口气让模型生成全套文案结果标题风格和卖点风格经常打架甚至会出现标题说“轻薄透气”卖点里又说“加厚保暖”这种自相矛盾的情况。分拆之后每个子任务可以拿到前面任务的结果作为输入比如卖点生成完标题生成时就可以引用卖点里的关键词保证全文信息一致。我这里的执行顺序是卖点先行再生成标题然后参数说明和场景描述并行最后售后说明单独生成。卖点是整个文案体系的锚点标题和场景描述都要围绕卖点展开。3.2 卖点挖掘让文案和商品图对齐卖点挖掘是整个详情页的灵魂但模型很容易把卖点写成废话什么“品质优良、舒适耐用”这种放之四海而皆准的句子。我的做法是把站点 A 的 visual_hints 作为硬约束传给文案 Agent要求每一个卖点必须来源于图片中可见的特征。比如图片里的商品有标签显示“100%棉”模型又识别出颜色是浅蓝场景是户外那我要求生成卖点时必须引用这些具体信息而不是泛泛地说“面料舒适”。我在 prompt 里加了一条每个卖点后面附上信息来源图片可见特征如果某一卖点在图上找不到对应依据就不能写入最终文案。这一步极大提升了文案和商品图的相关性也让后期人工审核轻松很多。3.3 合规过滤极限词必须拦淘宝对详情页的广告法合规管控非常严格极限词是红线绝对不能出现在文案里。包括但不限于“最”、“第一”、“国家级”、“顶级”、“独一无二”、“全网最低”等。大模型虽然知道一些违禁词但它不保证每个场合都记得尤其是生成营销文案时语气容易飘。我在文案生成站之后加了一道合规过滤层用 Go 的正则把文案跑一遍命中违禁词就记录下来并触发重写。重写不是简单把词替换掉而是把这个句子打回去让模型重新生成同时告诉模型命中了什么词、为什么违规。如果重写两次还是违规就转人工处理。这条硬性规则让详情页过审率提高了非常多我实际跑完 200 条链接没有被平台退回一次。var forbiddenWords []string{ 最有效, 第一品牌, 国家级, 顶级, 全网最低, 独一无二, 绝无仅有, // 实际清单远比这个长维护在一个独立文件里 } func CheckCompliance(text string) []string { var hits []string for _, w : range forbiddenWords { if strings.Contains(text, w) { hits append(hits, w) } } return hits }3.4 生成质量的控制手段文案生成还有一个容易忽略的点模型输出的长度控制。详情页的模块不同文案长度要求也不同。标题最多 30 个字卖点每条 20-40 字参数说明用表格形式输出。我会在 prompt 里给示例而不是只给字数要求。比如生成标题时我会给两个历史优秀标题作为 few-shot 示例模型对比着学风格比单纯说“写一个吸引人的标题”效果好非常多。另一个控制手段是温度参数。卖点生成和场景描述这种偏创意的内容温度给 0.8参数说明和售后说明这种偏事实的内容温度给 0.2。在 Go 代码里实现就是每个子任务单独传参数这个设计在后面整体调优时帮了大忙。4. 核心环节三详情页 HTML 的高效渲染4.1 详情页的模块化结构设计淘宝详情页虽然没有强制模板但买家的阅读习惯决定了它通常有固定的信息层级首屏是核心卖点陈述往下是产品参数表再往下是场景展示和细节展示最后是售后保障。我把这个结构沉淀为六个模块模板首屏横幅模块一句话核心卖点加主图卖点列表模块3-5 条卖点逐条展示参数表格模块规格参数以表格呈现场景描述模块结合使用场景讲一个故事图片展示模块放商品细节图售后保障模块退换货、保修等信息每个模块都是独立的 Go 模板文件这样运营想调整模块顺序时只需要改配置文件不用动代码。4.2 text/template 渲染的实战写法渲染层我用的是 text/template这个库比 html/template 更适合做详情页渲染因为详情页的 HTML 片段里会有 CSS、自定义 data 属性等html/template 会自动做 HTML 转义处理起来反而麻烦。text/template 直接替换变量只要保证输入数据是文案 Agent 生成的、经过合规过滤的就没有注入风险。模板示例大致如下const detailTpl div classdetail-banner h1{{.Title}}/h1 p{{.CoreSellingPoint}}/p /div div classdetail-specs table {{range .Specs}} trtd{{.Name}}/tdtd{{.Value}}/td/tr {{end}} /table /div {{if .SceneDesc}} div classdetail-scene p{{.SceneDesc}}/p /div {{end}} func RenderDetailPage(data DetailPageData) (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 } return buf.String(), nil }模块顺序编排我单独做了一个配置层用数组记录模块名列表渲染时按顺序拼接。这样日后增加新模块只需要新增模板文件不需要改渲染代码。4.3 图片资源与尺寸处理详情页里每张图片的尺寸直接影响加载速度和转化率。淘宝详情页推荐宽度是 750px超过这个宽度不仅加载慢还会被平台压缩影响清晰度。所以我在渲染站加了图片处理逻辑所有从商品图生成的素材图统一裁切到 750 宽度按模块用途分目录存储。首屏 banner 图我会把主图缩放到 750x750 并加上产品名称水印卖点图按比例裁剪。这里有个细节图片处理最好用纯 Go 的 image 库做不需要引入外部的 ImageMagick因为详情页素材的裁切都是简单几何操作纯 Go 完全够用部署也省事。资源落地以后详情页 HTML 里的 img src 用的是相对路径上传到 OSS 后再统一替换成 CDN 地址。这一步用 Go 做一个简单的路径替换函数即可不要直接渲染成绝对地址否则本地预览和线上部署会打架。5. 流水线的并发调度与容错5.1 goroutine errgroup 控制并发流水线天然适合并发站点 A 识别完类目之后站点 B 的五个文案子任务互相之间部分有依赖卖点要先出标题和场景描述依赖卖点但参数说明和售后说明完全独立可并行。我在 Go 里用 errgroup 来编排这部分既拿到并发能力又能统一收集错误。g, ctx : errgroup.WithContext(ctx) // 先并行生成卖点和参数说明 var sellingPoints, paramDesc, sceneDesc string g.Go(func() error { var err error sellingPoints, err generateSellingPoints(ctx, productInfo) return err }) g.Go(func() error { var err error paramDesc, err generateParamDesc(ctx, productInfo) return err }) if err : g.Wait(); err ! nil { return nil, err }errgroup 的一个特性是任何一个 goroutine 出错就会取消 context其他 goroutine 也会被中断退出这个特性对流水线来说非常合适失败就整体快速失败不要继续浪费 token。5.2 超时、重试和幂等调大模型接口必须设置超时否则一个慢请求会拖住整条流水线。我给每个环节设置独立的超时时间图像识别 30 秒文案生成每个子任务 60 秒整体流水线 5 分钟。超时之后进入重试逻辑采用指数退避加抖动第一次重试等 2 秒第二次 4 秒第三次 8 秒最多三次。重试要加幂等设计不然同一个商品图可能生成两份详情页。我给每个流水线任务生成一个 request_id在数据库里记录每个环节的状态同一个 request_id 重复进入流水线时直接返回已有结果。这个设计在并发回调、网络抖动重发时特别重要否则会造成脏数据。5.3 每个环节都要可观测流水线环节多、耗时长如果每个环节的耗时和 token 消耗不可见调优就是瞎摸。我引入 Go 1.21 标准库的 slog在每个环节入口和出口打日志记录 request_id、环节名、耗时、错误信息。跑完一批之后用脚本统计各环节耗时分布一眼就能看出瓶颈在哪里。实际跑下来发现80% 的时间花在文案生成上图像识别只占 12%。这说明如果要优化性能优先做文案并行如果要省钱优先优化 prompt 减少 token 消耗。没有日志这些结论都是拍脑袋。6. 上线效果与问题排查实录6.1 人工和流水线的对比说下实测数据。我拿 50 条真实商品链接做了对比测试人工制作的详情页平均耗时约 4 小时含设计、文案、排版流水线平均耗时 3 分 20 秒。运营对流水线初稿的采纳率第一次就能直接发布或只改少量文字的大约有 40%需要较多修改的大约 35%剩下一小部分因为商品图质量差或类目特殊需要重新拍摄或人工重写。这个采纳率看起来不高但如果算上流水线每分钟都在跑、失败可以立即重跑人只需要处理少数低质量产物整体效率提升非常明显。而且流水线最值钱的地方不是替代人而是把运营从重复劳动里解放出来让人只做最有价值的判断和创意工作。6.2 高频问题速查表这几个问题我基本上每条流水线都会遇到遇到频率最高的集中在下面这个表里现象根因解法模型输出 JSON 解析失败模型返回了多余解释或 markdown 代码块剥掉代码块再解析解析失败提示模型重新输出纯 JSON商品属性识别为 unknown 或明显错误图片分辨率太低或被过度压缩压缩宽度保底 1024对细节图走一次原图识别文案里出现违禁词模型生成营销话术时语气夸张合规过滤层用正则拦截并触发重写多任务并发时经常 429 限流并发数打满了模型服务端加信号量限制最大并发数控制在 5 以内详情页图片加载慢图片未压缩到 750 宽度体积过大渲染站统一裁切压缩转成 JPEG 质量 85同一张图重复生成多条详情页网络重试没有做幂等按 request_id 记录状态重复请求返回已有结果6.3 三个印象最深的坑第一个坑是模板渲染 HTML 转义问题。我第一次用 html/template 渲染详情页结果所有 CSS 里的属性选择器都被转义了样式全乱。后来换成 text/template 才正常。如果你也遇到这个问题检查一下用的模板库不要在一个库里死磕。第二个坑是模型输出和图片信息脱节。早期版本的卖点生成没有加“必须引用图片可见特征”的约束结果生成出来的卖点和商品完全不搭比如一条户外冲锋衣生成“适合办公室通勤”。加上硬约束之后这个问题基本消失但还是会偶发所以保留人工审核是必要的。第三个坑是并发一开就限流。最开始我用 errgroup 把五个文案子任务全部并行结果模型服务端频繁返回 429整体耗时反而变长了。后来加了一个信号量限制同时最多 3 个请求在跑429 基本消失总耗时反而下降了 30%。并发不是越大越好服务端的配额限制要考虑进去。根据我目前跑这条流水线的体会后续还可以做两个自然的扩展一是把类目知识库从 JSON 文件换成数据库支撑更大规模的商品接入二是给每个详情页生成 A/B 版本标题让运营做点击率测试反哺文案 prompt 的调优。不过说到底Agent 流水线是越简单越可靠环节加得越多故障点就越多。现阶段这套系统已经能让运营把精力花在真正需要人的地方这就是我认为它最大的价值。