这个项目从立项到完结差不多做了三个多月做的是一个把 AI 修图能力包装成 Agent 服务的全栈项目。用户在 Web 端、小程序端甚至 App 端输入一句“把背景换掉调亮一点”“去水印”“把这张图做旧”后端 Golang 编排的 Agent 会自动拆解指令、调用图像处理服务、跑模型并返回成图。整个过程用户不需要理解图层、蒙版、曲线这些概念只需要说人话。项目完结后我一直在想这类“全栈 AI 修图 Agent”比传统修图工具到底多解决了什么问题答案不是修图本身而是把“修图”从功能变成了流程这也是我写这篇复盘的原因。适合正在做全栈项目、AI Agent 落地、或者想把大模型接进实际业务的人参考里面没有太玄的东西全是能直接落地的方案和踩坑记录。1. 从修图需求到 Agent 化这个项目到底在做什么1.1 为什么想做“AI 修图 Agent”而不是又一个修图 App市面上修图工具已经足够多美颜、滤镜、抠图、调色单拎出来每一项都有成熟产品。但这类工具的交互逻辑是“用户自己选功能、自己调参数”修图能力是离散的。用户真正想要的是结果不是工具栏。很多人拍了一张图想的是“帮我处理一下”而不是“先抠图、再调色、再锐化”。Agent 的价值恰恰在这里它把离散的修图能力串成了可自主决策的流程。这个项目定位是“多端全栈实训性质的真实业务系统”技术栈固定为 Vue3 Golang Uniapp AI目标不是做一个研究型 Demo而是让 Agent 能真实处理一批修图请求同时覆盖 Web、小程序和 App 三端。前端的交互形式统一为“对话式修图”用户发一张原图加一句指令Agent 端组装任务、调度模型、回传结果。1.2 Agent 修图和传统“一键修图”的差别一键修图通常是一个固定模型或固定脚本输入图片输出结果中间没有决策。比如“一键抠图”就是永远执行同一个分割模型用户输入图片得到透明背景图。它的问题是缺乏上下文理解无法根据图片内容动态选择处理策略。Agent 修图加了一层“大脑”。它先看用户指令再结合图片的元信息、可用的工具列表、历史操作记录决定这次要执行哪些步骤。同样是“把背景换掉”对一张人像图和一张商品图Agent 选择的工具和参数会完全不同。人像图要跑人像分割商品图可能跑通用分割或者边缘检测。更关键的是Agent 可以处理复合指令“抠图 替换背景 把主体调亮 输出透明背景 PNG”这不是一个模型能一步搞定的是多个模型按顺序协作的结果。这个“多步骤决策 工具调用 中间结果反馈”的链路就是它和传统一键修图的本质区别。1.3 项目全景多端、全栈、一条服务链路整个项目的架构可以分成四层前端层Vue3 构建 Web 页面Uniapp 构建小程序和 App统一对话界面、图片上传、结果展示。接入层Golang 提供 HTTP API处理鉴权、文件上传、任务创建、WebSocket 推送进度。Agent 编排层Golang 实现的 Agent Engine负责与 LLM 交互决定调用哪些修图工具维护多轮上下文。图像处理层一组独立部署的 Python 服务封装抠图、调色、增强、修复、格式转换等能力通过 HTTP 接口被 Agent 调用。这个链路跑通之后从用户点上传到看到结果中间经历过“前端上传 - 后端存储 - Agent 拆解指令 - 选择工具 - Python 服务执行 - 结果回传 - 前端渲染”七个环节。项目的难点不在于单个环节而在于让这条链路稳定、可控、可观测。2. 技术栈为什么这样搭Vue3 Golang Uniapp 的分工逻辑2.1 为什么后端选 Golang并发、部署、Agent 编排的舒适度先说结论Golang 不是唯一选择但在这个项目里非常合适。核心原因有三个。第一个是并发模型。Agent 服务面对的不是“用户请求一次就算了”而是一句话可能触发多个工具调用每个工具调用都要消耗几十毫秒到几秒Agent 还需要在工具调用之间做循环决策。Golang 的 goroutine 让并发任务编排变得很轻每个请求可以由独立的 goroutine 组处理互不干扰资源占用远小于 Java 的线程模型。第二个是部署与运维。项目同时要跑 Web 服务、Agent Worker、文件服务Golang 编译出来是单个二进制文件部署到服务器上不需要装 JRE、不需要配置容器特别适合实训项目和多端联调阶段频繁迭代的环境。第三个是 Agent 编排的落地手感。Agent 的本质是一个循环LLM 返回结构化内容 - 解析意图 - 调用工具 - 把结果塞回上下文 - 再让 LLM 决策。这个循环在 Golang 里非常适合用接口类型实现每个 Tool 都是一个接口注册进去之后由 Engine 统一调度代码组织非常清晰。2.2 前端为什么用 Vue3 UniappWeb 与小程序一套代码前端选 Vue3 Uniapp直接原因是需要覆盖三端但不想维护三套代码。Uniapp 的编译模式可以一套代码同时输出 H5、微信小程序和 App配合 Vue3 的组合式 API状态管理、组件复用、条件编译都相对成熟。在真实开发中Uniapp 并不是完全没有坑这部分后面单开一节细讲。但就项目整体而言三端共用一个代码仓库大大降低了实训项目的维护成本。特别是 Agent 对话界面本质上由“消息列表 图片组件 输入框”三个核心模块组成一套组件在 H5 和小程序上的表现差异并没有想象中那么大。前端与后端交互统一走 HTTP API图片上传采用“先拿临时凭证 - 直接传到对象存储 - 后端保存文件路径”的模式。Agent 返回结果后前端通过 WebSocket 接收进度事件实时展示“正在分析指令”“正在抠图”“正在调色”这类中间状态避免用户等待时不知道系统在干什么。2.3 AI 模型接入的落地方案本地模型还是云端 API这是项目中比较纠结的一个决策。修图 Agent 涉及两类 AI 能力一类是 LLM 决策能力负责理解指令、选择工具、规划步骤另一类是图像处理能力负责抠图、调色、增强、超分等。LLM 部分一开始考虑过本地部署开源模型但在三端联调阶段发现本地模型在复杂指令理解、多轮对话一致性上的表现不稳定尤其是用户说“把这里调一下但不要动那边”这类含糊指令时开源小模型经常误解。最后还是选择云端大模型 API换来的是更稳定的决策质量和更少的 Prompt 调试时间。图像处理部分则全部本地化。原因很直接图像服务需要处理的是用户真实上传的图片存在延迟敏感和隐私问题而且抠图、分割这类模型经过量化和优化后在单张消费级 GPU 上就能跑得不错不需要依赖外部接口。最终形态是“云端 LLM 做决策本地图像模型做执行”这也符合大多数 AI Agent 项目的落地现状。决策模型要聪明执行模型要私有两层解耦之后后续替换任何一个模型都不影响整体架构。3. AI Agent 修图的核心工具调用链与图像处理 Pipeline 设计3.1 Agent 如何“听懂”一句人话修图指令Agent 理解指令不是靠 NLP 分词而是靠结构化 Prompt 函数调用Function Calling。系统收到用户指令后会先把“图片基本信息”和“可用工具列表”一起交给 LLM让 LLM 输出一个 JSON 结构而不是自由文本。工具列表在 Prompt 中是这样组织的{ tools: [ { name: remove_background, description: 去除图片背景生成透明背景 PNG, parameters: { type: object, properties: { image_url: {type: string}, model: {type: string, enum: [person, general]} }, required: [image_url] } }, { name: adjust_brightness, description: 调整图片整体亮度, parameters: { type: object, properties: { value: {type: integer, minimum: -100, maximum: 100} }, required: [value] } } ] }LLM 的输出大致是这样的{ thought: 用户想换背景需要先抠图再替换, steps: [ {tool: remove_background, params: {image_url: ...}}, {tool: replace_background, params: {color: #E8F4FD}} ] }这里有个容易忽略的细节Agent 的意图拆解不一定一次性完成。有些指令非常明确LLM 一次就能输出完整步骤有些指令含糊比如“感觉有点怪帮我弄好看点”这种情况 Agent 会先输出一个中间问题向用户确认“你是希望调色还是加滤镜”多轮对话的上下文管理和工具调用一样重要后面会展开。3.2 工具注册与多轮调用从 LLM 决策到服务执行Agent Engine 在 Golang 里的核心是一个循环调度器。我用一个接口抽象所有工具type Tool interface { Name() string Description() string Parameters() map[string]interface{} Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) }每个图像处理能力都实现这个接口比如RemoveBackgroundTool、AdjustColorTool、EnhanceTool、CompressTool。Agent Engine 启动时把所有 Tool 注册到 map 里LLM 返回的 steps 会映射到对应 Tool 对象然后逐个执行。执行过程中最关键的机制是“工具结果回填”。LLM 决策后会调用工具但工具执行完的结果不能直接丢给用户而要重新组成一条消息塞回 LLM 的上下文中让 LLM 判断下一步是继续调用工具还是结束。这就是 ReAct 模式的基本形态用户输入指令和图片Agent 把工具列表和对话历史发给 LLMLLM 输出工具调用计划Agent 执行工具得到结果Agent 把结果回填给 LLMLLM 判断任务是否完成完成则输出最终回复否则回到第 3 步实际项目中一个修图任务平均会经历 2 到 4 轮工具调用最长的一次我见过 8 轮用户说“帮我修一下你就是设计师你觉得哪不好看就改哪”LLM 连续触发了裁切、调色、增强、锐化四个工具。3.3 图像处理 Pipeline 的拆解抠图、调色、增强、合成图像处理层跑在 Python 服务里提供类似/api/v1/remove-background、/api/v1/adjust-color这样的 REST 接口。每个接口内部是一个独立的模型或算法管线。以“抠图 替换背景 调亮主体”这个最常见场景为例完整 Pipeline 是第一步人像分割模型跑一次得到 alpha 蒙版输出透明背景 PNG。第二步把透明 PNG 和新背景层叠加合成新图。第三步对主体区域做亮度调整只调整前景不动背景。这里有一个团队里反复踩过的点图像处理的执行顺序不是 Agent 随便指定的。Agent 可以先调亮再抠图也可以先抠图再调亮结果几乎一样但“替换背景”必须在“抠图”之后如果 Agent 先替换背景再抠图会把新背景一起抠掉。所以工具列表里的 description 必须写清楚依赖关系或者在 Engine 层做任务排序。为了让描述足够清晰我在工具描述里用了很多“只能在 X 之后调用”的约束语句。LLM 对这类顺序关系的理解是可以通过 Prompt 约束的但必须给足上下文。这也是很多 Agent 项目做不好的地方——工具描述太简短模型根本不知道什么时候该用哪个工具。3.4 状态管理与异步任务长时间修图任务怎么不卡请求图像任务耗时差异巨大。调亮度可能 100 毫秒就完成超分或修复可能要 5 秒以上如果再加上多步骤组合整个 Agent 流程可能要 10 秒甚至更久。如果前端用同步请求等待响应体验会非常差。这个项目采用了“任务制 异步轮询/推送”的设计用户上传图片后前端先调/api/v1/tasks创建任务得到task_id。Agent Engine 异步处理每一步的状态都写入 Redis比如pending、tool_running、tool_success、completed。前端通过 WebSocket 订阅task_events通道实时收到“正在执行抠图”“正在调色”等事件。全部工具执行完毕后后端把最终图片 URL 写入任务结果前端推送事件触发渲染。这个设计让用户感受到的不是一个等待中的请求而是一个可以观察进度的“工作流”。产品体验上差异非常明显也方便后续接入任务重试、失败恢复。4. 多端联调与真实踩坑Web、小程序、App 的差异化处理4.1 uniapp 条件编译与平台差异uniapp 的“一套代码多端运行”在上手时很爽但越做到后面越会发现三端之间仍然存在不少需要特判的地方。项目里用条件编译解决了一部分比如小程序端不能随便用 DOM APIH5 端对 WebSocket 的支持更稳定App 端拍照上传走的是chooseImage返回的临时文件路径。最典型的一个差异是图片选择。Web 端用input typefile小程序端用uni.chooseImage()App 端如果嵌的是 web-view 则又不一样。条件编译的写法是这样的// #ifdef MP-WEIXIN const res await uni.chooseImage({ count: 1 }) const filePath res.tempFilePaths[0] // #endif // #ifdef H5 const input document.createElement(input) input.type file // #endif这段代码写完之后我发现真正的麻烦不是选择图片本身而是各端拿到文件后的后续处理。H5 端uploadFile可以直接传 File 对象小程序端需要传tempFilePathApp 端某些版本对 file path 的格式要求还不一样。所以我在项目中封装了一层uploadImage()统一入口内部做平台判断。4.2 小程序里的大文件上传与下载坑小程序的上传和下载是最容易出问题的地方。微信小程序的wx.uploadFile默认超时时间是 60 秒文件大小上限是 10MB但如果用户上传的是高清原图一个文件很容易超过 10MB。我们在项目里做了两层处理第一层是前端压缩在用户选择图片后如果文件超过 2MB先通过 canvas 进行等比压缩再上传。第二层是后端限制放宽Golang 服务端把请求体大小上限调到了 20MB但仍然建议用户上传压缩后的图片因为图像模型处理超大原图的速度会明显下降。下载侧也有坑。处理后的图片如果直接返回对象存储的永久链接在小程序里是可以直接通过image组件展示的但如果生成的是临时链接就必须先调用uni.downloadFile保存到本地再展示。最容易忽略的是 Android App 端临时链接直接赋值给 image 组件有时不刷新需要在链接后面加时间戳参数强制刷新。4.3 图片代理与存储策略跨域、CORS、临时链接多端项目里图片的获取远比本地项目复杂。Web 端有跨域限制小程序端有域名白名单校验App 端相对宽松但本地路径和网络路径混用容易混乱。这个项目里图片存储用的是对象存储后端在返回结果时生成带签名的临时链接有效期 30 分钟。三个端对临时链接的支持程度不同H5 端直接访问没问题小程序端要求域名在后台配置合法域名否则报downloadFile:fail url not in domain list。开发阶段我把这个限制临时关闭了但上线前必须配置好。跨域问题的解决方案是在 Golang 中间件里统一加 CORS 头func CORS() gin.HandlerFunc { return func(c *gin.Context) { c.Header(Access-Control-Allow-Origin, *) c.Header(Access-Control-Allow-Methods, GET, POST, PUT, DELETE, OPTIONS) c.Header(Access-Control-Allow-Headers, Content-Type, Authorization) if c.Request.Method OPTIONS { c.AbortWithStatus(204) return } c.Next() } }注意这里的Access-Control-Allow-Origin: *只适合开发或非敏感场景如果涉及用户私有图片最好是后端维护一个白名单域名列表动态返回具体来源域而不是简单用*否则任何第三方网站都能跨域读取你的图片接口。5. 实测效果与性能数据修一张图需要多久质量怎么样5.1 端到端时延拆解项目完结前对整套系统做了多轮压测重点看一张图从上传到最终结果返回的耗时分布。以一张 1920x1080 的人像图为测试样本请求“抠图 更换背景 提亮”完整时延大约 8.2 秒其中各环节的耗时拆解如下环节耗时占比图片上传与存储0.8 秒9.8%Agent 指令拆解LLM1.5 秒18.3%抠图模型推理1.8 秒21.9%背景替换与合成0.6 秒7.3%亮度调整0.3 秒3.7%Agent 结果回填LLM1.2 秒14.6%结果回传与前端渲染2.0 秒24.4%可以看到 LLM 的调用占了很大一块两次调用加起来 2.7 秒。后期如果追求更低的延迟可以把第一次指令拆解和第二次结果回填合成一次调用让 LLM 在一次输出中既规划步骤又在工具执行后补充总评。不过这样会牺牲一部分多轮决策的准确性属于取舍问题。图片上传和前端渲染的 2.8 秒也存在优化空间比如上传改用分片并发结果渲染先用缩略图占位再加载原图体感延迟能降低到 5 秒以内。5.2 质量评测人工打分与对比模型的效果不是这篇复盘的侧重点但作为全栈项目图像处理质量直接影响用户体验。项目里我对四个核心场景各选了不少于 20 张测试图做人工打分评分维度是“背景分割边缘准确度”“调色自然度”“整体美观度”和“与原图相似度”每项 1 到 5 分。人像抠图边缘准确度平均 4.2 分商品图抠图相对低一些只有 3.6 分。原因很直观商品图种类太杂瓶装饮料、电子产品、服装鞋帽的数据分布差异大通用分割模型在边缘反光、透明物体、细碎结构上还是会翻车。这里我给用户加了一个兜底方案如果 Agent 识别出图片属于“物体边缘复杂”的类型会自动追加一步“边缘羽化 手动微调提示”但这已经不是纯自动流程了。调色类操作的主观性很强两个用户对同一张图的“自然”定义可能完全相反。Agent 在这类任务上只做保守调整默认把亮度、饱和度变化幅度控制在 20% 以内避免输出过度的“滤镜感”。5.3 成本控制Token 消耗与模型选择云端 LLM 的成本在这个项目里是一个绕不开的话题。一次完整修图流程平均消耗 Token 数量在 2500 到 3500 左右其中图片描述信息的 Base64 编码会占用大量 Token。成本优化的做法有三个减少图片信息的冗余描述。上传图片后后端先生成一张 256px 的缩略图用视觉模型生成简短的图片描述再把这段描述作为 LLM 的输入而不是直接把原图塞进去。工具结果不完整回填。抠图工具执行成功后只需要把“工具执行成功 输出文件路径”回填给 LLM不需要把图片的 Base64 内容再次发给 LLM。很多 Agent 项目在这里犯了一个大错误——把所有工具结果原样塞回上下文导致 Token 暴涨。对话历史做截断。多轮对话超过 6 轮后把早期的消息摘要成一段话只保留最近两轮的完整消息。实测下来这三个优化让单任务的 Token 消耗降低大概 40%成本控制效果非常明显。如果你的项目对成本敏感建议从“工具结果回填”这一步入手大部分时候工具返回的结果只是一个确认信号不需要完整内容。6. 回顾这个项目的几个关键决策与后续演进6.1 我踩过的坑Agent 工具返回格式不稳定所有踩过的坑里最值得单独拿出来说的是“Agent 工具返回格式不稳定”。LLM 有概率在输出 JSON 时多一个逗号、少一个闭合括号或者字段名拼写和定义不完全一致直接解析就会报错。项目里我写了两个兜底逻辑。第一个是“解析失败自动纠错”用正则把常见的错误模式替换掉比如去掉最后一个逗号、把单引号替换成双引号。第二个是“重试机制”如果纠错后仍然解析失败把错误信息拼进下一轮 Prompt告诉 LLM“你上次输出格式错误请重新按 JSON 输出”实测重试一次的成功率能到 98% 以上。还有一个经典问题是工具名称不一致。我定义的工具是remove_background但 LLM 偶尔会输出removeBackground或remove-bg。解决方案是在工具注册时维护一组别名或者在 Prompt 里明确强调“工具名称只能从列表中选择禁止修改”。两种方法我都用了别名兜底更保险。6.2 后续可以扩展的方向第一期完结之后这个项目还能往几个方向走。第一个是接入更多图像工具比如图生图、局部重绘、老照片修复Agent 能调用的工具越多能承接的复合任务就越复杂。第二个是做用户偏好学习记录用户每次修图后的反馈把“喜欢更亮的调色”“倾向冷色调背景”这类偏好写进用户画像在后续 Agent 决策时作为参考这是传统修图工具很难做到的个性化。第三个是任务工作流的可视化编排让高级用户自己拖拽工具节点组合出专属修图流程Agent 只负责解释和调度用户自定义的工作流。从这次项目的实际体验来看全栈 AI 修图 Agent 的复杂度不在模型本身而在如何把模型能力准确地编排成用户可理解、可信任的结果。Agent 不是万能钥匙但它让我看到工具软件交互方式正在从“功能菜单”走向“意图对话”的方向。下一期如果继续做我可能会把更多精力放在多轮对话的上下文压缩和工具失败后的自愈机制上这两个点直接决定了 Agent 能不能从“演示可用”变成“日常可用”。
