Go+AI Agent实战:一张商品图生成整套电商详情页
1. 为什么我盯上了一张商品图生成整套详情页这件事做电商的朋友应该都有体会详情页是个磨人的活儿。一张主图拍完运营要写卖点文案、设计要排版、美工要抠图换背景、最后还要拼成一套符合平台规范的详情页长图。一个 SKU 走完这套流程快的话半天慢的话两三天SKU 一多人力成本直接爆炸。我最早的想法很朴素能不能让 AI 把写文案和排版这两步自动化后来发现市面上的工具要么是纯文案生成生成完还得手动排版要么是纯设计工具模板套用文案还得自己填中间那道从图到成品的缝一直没人缝上。于是我就琢磨着用 Go 搭一条 AI Agent 流水线把整条链路串起来输入一张商品图输出一整套可以直接用的详情页素材。这条流水线涉及几个核心关键词Go、AI Agent、Wails、Vue3、GLM-4.6V。Go 负责后端调度和 Agent 编排Wails 把 Go 和 Vue3 打包成一个桌面应用Vue3 做前端交互界面GLM-4.6V 这个多模态模型负责看图说话——识别商品、提取卖点、生成文案。整套东西跑下来从上传图片到拿到详情页大概两三分钟。这篇文章我会把整条流水线的设计思路、Agent 编排逻辑、Wails 集成踩的坑、GLM-4.6V 的调用细节全部摊开讲。适合两类人看一是想用 Go 做 AI Agent 应用开发但不知道从哪下手的二是做电商工具、想了解多模态模型怎么落地到具体业务场景的。代码和配置我会尽量给全能抄作业的地方直接抄。先说清楚一个概念很多人搞不清AI Agent 和 LLM 的区别。LLM大语言模型是大脑你问它答它本身不会主动做事AI Agent 是大脑 手脚 记忆它能自己决定调用什么工具、按什么顺序执行、失败了怎么重试。我这条流水线里GLM-4.6V 是那个大脑Go 写的调度器是手脚中间的任务状态管理是记忆。理解了这层关系后面的设计就顺了。2. 整条流水线的骨架Agent 到底分几步干活2.1 把生成详情页拆成四个可独立执行的 Agent一开始我想用一个超级 Prompt 让模型一步到位结果发现根本不可控——模型要么漏掉卖点要么排版乱套而且一旦中间某步出错整个流程得重来。后来我改成分而治之把任务拆成四个职责单一的 AgentAgent 名称职责输入输出视觉理解 Agent识别商品类别、颜色、材质、场景商品原图结构化商品描述 JSON卖点提炼 Agent基于商品描述生成营销卖点商品描述 JSON5-8 条卖点文案文案生成 Agent生成标题、副标题、详情段落卖点列表完整文案包排版组装 Agent按模板把文案和图片拼成详情页文案包 原图详情页长图这么拆的好处是每个 Agent 的 Prompt 都能写得非常聚焦输出格式也好约束。比如视觉理解 Agent 我只让它输出 JSON不让它写任何自然语言段落这样下游解析起来零歧义。2.2 为什么用 Go 做编排而不是 Python很多人做 AI 应用第一反应是 Python生态确实好。但我选 Go 有几个实在的理由第一这条流水线要打包成桌面应用给运营用Go 编译出来是单个二进制Wails 直接能嵌Python 打包成 exe 那个体积和启动速度你懂的第二Agent 编排本质是并发调度Go 的 goroutine 写起来比 Python 的 asyncio 顺手太多第三我要调多个模型接口Go 的 HTTP 客户端和超时控制更干净。具体到编排层我用了一个简单的 DAG有向无环图结构。每个 Agent 是一个节点节点之间有依赖关系。视觉理解是根节点卖点提炼依赖它文案生成依赖卖点提炼排版组装依赖文案生成。用 Go 实现就是一个带 channel 的调度器type AgentNode struct { Name string Run func(ctx context.Context, input map[string]any) (map[string]any, error) Depends []string } type Pipeline struct { nodes map[string]*AgentNode done map[string]chan map[string]any }每个节点跑完把结果塞进自己的 channel下游节点从依赖的 channel 里取数据。这样天然支持并行——比如以后我想加一个竞品分析 Agent和卖点提炼 Agent并行跑改依赖关系就行不用动调度逻辑。2.3 任务状态怎么存别小看这一步Agent 流水线最怕的就是跑到一半挂了不知道跑到哪了。我一开始图省事把状态放内存里结果应用一崩全丢用户得从头再来。后来改成用 SQLite 落盘每个 Agent 的执行状态、输入、输出、耗时都记一张表CREATE TABLE agent_runs ( id INTEGER PRIMARY KEY, task_id TEXT, agent_name TEXT, status TEXT, -- pending/running/success/failed input_json TEXT, output_json TEXT, error_msg TEXT, started_at DATETIME, finished_at DATETIME );有了这张表断点续跑就简单了——重新执行时先查哪些节点已经 success直接跳过。这个设计在调试阶段帮了我大忙某个 Agent 输出不对我直接看表里的 output_json 就知道问题出在哪不用加一堆日志重新跑。提示状态落盘别用 JSON 文件并发写会出问题。SQLite 开 WAL 模式单机场景完全够用比上 Redis 省事。3. GLM-4.6V 怎么用才不浪费多模态调用的实战细节3.1 视觉理解 Agent 的 Prompt 设计GLM-4.6V 是这条流水线的核心它决定了后面所有环节的质量。我踩的第一个坑就是 Prompt 写得太开放比如请描述这张商品图模型会给你写一段散文什么画面中呈现出一件精美的...这种输出下游根本没法解析。正确的做法是强约束输出格式。我最终的 Prompt 是这样的你是一个电商商品分析助手。请分析这张商品图片严格按以下 JSON 格式输出不要输出任何其他内容 { category: 商品类目, color: [主色, 辅色], material: 材质推测, style: 风格标签, scene: 使用场景, target_user: 目标人群, key_features: [外观特征1, 外观特征2] }关键是最后那句不要输出任何其他内容。即便这样模型偶尔还是会加个好的以下是分析结果的前缀。所以我在 Go 侧做了解析容错——用正则先提取第一个{到最后一个}之间的内容再反序列化。这个容错逻辑看起来土但实测比任何请严格输出 JSON的咒语都管用。3.2 图片怎么传base64 还是 URLGLM-4.6V 支持两种图片传入方式base64 编码和图片 URL。我两种都试过结论是本地图片用 base64网络图片用 URL。base64 的坑在于体积——一张 2MB 的图编码后接近 2.7MB请求体一大超时概率直线上升。我的处理是在 Go 侧先压缩把图片长边压到 1024px质量降到 85%这样一张图通常能压到 200KB 以内识别效果几乎没损失。func compressImage(src string) ([]byte, error) { img, err : imaging.Open(src) if err ! nil { return nil, err } // 长边限制 1024 img imaging.Fit(img, 1024, 1024, imaging.Lanczos) var buf bytes.Buffer err imaging.Encode(buf, img, imaging.JPEG, imaging.JPEGQuality(85)) return buf.Bytes(), err }这里用的是github.com/disintegration/imaging轻量好用。压缩这步千万别省我做过对比不压缩的请求失败率大概 8%压缩后降到 1% 以下。3.3 多模态模型的幻觉怎么防多模态模型有个通病你问它材质它看图猜不出来也会硬编一个。比如一件纯棉 T 恤它可能说成涤纶混纺。这种幻觉在电商场景是致命的写进详情页就是虚假宣传。我的应对策略是区分看得见的和猜的。Prompt 里明确要求颜色、外观特征这类肉眼可见的必须准确材质、成分这类看不见的如果无法确定就填未知不要编。同时在 Go 侧加了一层校验——如果material字段是未知文案生成 Agent 就跳过材质相关的卖点不硬写。这个设计思路其实适用于所有多模态落地场景让模型只做它擅长的事不确定的部分交给规则兜底。指望模型百分百准确是不现实的工程上的价值在于把它的不确定性控制在可接受范围内。4. Wails Vue3 把流水线装进桌面应用4.1 为什么是 Wails 而不是 ElectronElectron 打包出来动辄一两百 MB启动还慢。Wails 的思路是Go 做后端 系统 WebView 做前端打包出来通常 10-20MB启动秒开。对于我这种后端逻辑重、前端只是操作界面的场景Wails 简直量身定做。Wails 的核心机制是绑定你在 Go 里定义一个 struct把方法暴露出去前端 Vue3 里就能直接await调用像调本地函数一样。比如我的流水线入口type App struct { ctx context.Context pipeline *Pipeline } func (a *App) GenerateDetailPage(imagePath string) (string, error) { return a.pipeline.Run(a.ctx, imagePath) }前端就这么调import { GenerateDetailPage } from ../wailsjs/go/main/App const result await GenerateDetailPage(/path/to/image.jpg)没有 HTTP 接口、没有跨域、没有序列化烦恼数据直接在进程内传递。这是 Wails 相比Go 起个 HTTP 服务 前端 fetch最大的优势。4.2 Vue3 前端的状态管理别把 Agent 进度搞丢流水线跑起来要两三分钟用户得能看到进度。我在 Vue3 侧用 Pinia 管理任务状态每个 Agent 的状态变化通过 Wails 的 Events 机制从 Go 推送到前端// Go 侧推送事件 runtime.EventsEmit(a.ctx, agent:progress, map[string]any{ agent: 视觉理解, status: running, progress: 25, })// Vue3 侧监听 import { EventsOn } from ../wailsjs/runtime/runtime EventsOn(agent:progress, (data) { taskStore.updateAgentStatus(data.agent, data.status, data.progress) })这里有个坑Wails 的事件是异步的如果前端组件卸载了但事件还在推会报错。我的做法是在onUnmounted里把监听器取消掉或者用 Pinia 的 store 承接事件组件只读 store这样组件卸载也不影响。4.3 打包时最容易翻车的几个点Wails 打包我踩的坑能写一篇。第一个是WebView2 依赖Windows 上如果用户系统没装 WebView2 Runtime应用直接白屏。解决办法是在安装包里带上 WebView2 的引导安装或者检测到缺失时提示用户。第二个是资源路径开发时图片路径是相对路径打包后工作目录变了得用os.Executable()拿到二进制位置再拼绝对路径。第三个是图标和版本信息Wails 的wails.json里配置别等打包完才发现图标是默认的。注意Wails 打包前一定要在干净环境测一遍尤其是没装过开发环境的机器。我遇到过开发机跑得好好的换台机器就白屏最后发现是 WebView2 版本太老。5. 从能跑到好用那些只有真跑起来才知道的事5.1 并发调用模型接口的限流流水线里四个 Agent 串行跑但每个 Agent 内部可能有多张图要处理比如一个商品有主图、细节图、场景图。如果无脑并发很容易触发模型接口的 QPS 限制。我在 Go 侧加了个简单的令牌桶限流limiter : rate.NewLimiter(rate.Limit(5), 10) // 每秒5个突发10个 func callModel(ctx context.Context, req Request) (Response, error) { if err : limiter.Wait(ctx); err ! nil { return Response{}, err } // 实际调用 }用golang.org/x/time/rate就行几行代码解决。限流阈值要根据你实际购买的接口配额调别照抄我的 5。5.2 失败重试的度怎么把握模型接口偶尔超时、偶尔返回格式错误重试是必须的。但重试不能无脑重试——我见过有人写个for i : 0; i 10; i死循环重试结果接口挂了直接把配额刷爆。我的策略是指数退避 最大次数 区分错误类型。超时、5xx 这类可重试重试间隔 1s、2s、4s格式解析错误这类不可重试直接失败让上游处理。最大重试 3 次超过就标记该 Agent 失败走降级逻辑比如视觉理解失败就用默认模板文案。func retryWithBackoff(fn func() error, maxRetries int) error { for i : 0; i maxRetries; i { err : fn() if err nil { return nil } if !isRetryable(err) { return err } time.Sleep(time.Duration(1i) * time.Second) } return fmt.Errorf(重试 %d 次后仍失败, maxRetries) }5.3 成本控制别让流水线变成烧钱机器多模态模型按 token 计费图片也占 token。一条流水线跑下来如果图片不压缩、Prompt 不精简成本能差好几倍。我做了几件事图片压缩前面说过、Prompt 复用把不变的指令部分做成常量只拼可变部分、结果缓存同一张图算过的视觉理解结果存下来重复上传直接读缓存。缓存这块用图片的 MD5 做 key存 SQLite。实测下来运营反复调整文案时视觉理解这步基本都命中缓存省了一大笔。5.4 输出质量的人工兜底再好的模型也有翻车的时候。我在前端加了个人工审核环节——流水线跑完不直接导出而是把生成的文案和排版展示出来运营可以逐条编辑、替换、删除确认无误再导出。这个设计看起来是AI 不够智能的妥协但实际上极大提升了可用性。运营反馈说有了这个环节他们敢用了因为知道最终把关的还是自己。6. 这套架构还能往哪扩跑通这条流水线之后我发现这套Go 编排 多模态 Agent Wails 桌面端的架构其实很通用。往小了说把视觉理解 Agent 换成文档解析 Agent就能做合同摘要工具把排版组装 Agent 换成PPT 生成 Agent就能做自动做汇报材料。往大了说Agent 之间的依赖关系可以更复杂加个质量评分 Agent做自动质检加个多方案生成 Agent一次出三版让用户选。我最近在试的一个方向是把 Agent 做成可插拔的——每个 Agent 定义成接口用户可以在配置里自由组合。这样同一套壳子换个 Agent 组合就是另一个产品。Go 的接口机制做这个天然合适type Agent interface { Name() string Depends() []string Execute(ctx context.Context, input map[string]any) (map[string]any, error) }只要实现这个接口就能塞进流水线。这个设计我还在打磨等稳定了再单独写一篇。最后分享一个我踩了很久才想明白的点AI Agent 项目的难点从来不在模型调用而在工程化。模型接口谁都会调但怎么让整条链路稳定、可观测、可恢复、成本可控这才是真正拉开差距的地方。我这条流水线里真正花时间的代码是状态管理、重试、限流、缓存这些不性感的部分模型调用反而只占很小一块。如果你也在做类似的东西别一上来就纠结用哪个模型先把工程骨架搭扎实模型随时能换骨架换起来可就伤筋动骨了。