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 让模型一步到位,结果发现根本不可控——模型要么漏掉卖点,要么排版乱套,而且一旦中间某步出错,整个流程得重来。后来我改成"分而治之",把任务拆成四个职责单一的 Agent:
| Agent 名称 | 职责 | 输入 | 输出 |
|---|---|---|---|
| 视觉理解 Agent | 识别商品类别、颜色、材质、场景 | 商品原图 | 结构化商品描述 JSON |
| 卖点提炼 Agent | 基于商品描述生成营销卖点 | 商品描述 JSON | 5-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 还是 URL
GLM-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 而不是 Electron
Electron 打包出来动辄一两百 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(1<<i) * 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 项目的难点从来不在模型调用,而在工程化。模型接口谁都会调,但怎么让整条链路稳定、可观测、可恢复、成本可控,这才是真正拉开差距的地方。我这条流水线里,真正花时间的代码是状态管理、重试、限流、缓存这些"不性感"的部分,模型调用反而只占很小一块。如果你也在做类似的东西,别一上来就纠结用哪个模型,先把工程骨架搭扎实,模型随时能换,骨架换起来可就伤筋动骨了。