
刚好上周把修图Agent的最后一个版本合到主干前端、后端、AI编排、多端入口全部打通这个全栈AI修图Agent项目算是真正完结了。趁热做个复盘把整个项目的设计思路、技术选型、Agent机制拆解过程以及实际推进中踩过的坑都整理出来。如果你正准备做一个类似的全栈AI应用或者正在纠结要不要从后端往Agent开发方向转这篇文章应该能给你一个比较完整的参考样本。我这不是那种“调个API完事”的demo型项目。整个项目覆盖了Vue3搭建的PC管理端、Golang实现的Agent编排服务以及基于Uniapp的移动端入口最终目标是让用户通过一句自然语言指令比如“把这张照片调亮一点顺便去掉背景里的垃圾桶”Agent就能自主完成意图拆解、工具调用、像素级修图操作并把结果回传到多端展示。这里的核心不是“修图”本身而是“Agent怎么理解任务、编排任务、执行任务”。1. 项目定位与整体设计1.1 我们要做的不是“滤镜工具”而是一个会自己干活的智能体很多人一听“AI修图”第一反应是套滤镜、磨皮、调亮度那其实是传统图像处理App做的事。这个项目的定位完全不同我们做的是一个具备自主决策能力的Agent用户给的是一个目标Agent需要自己去拆解这个目标选择合适的工具链逐步执行最后交付成品。举个例子用户上传一张逆光环境下拍的人像照片说“帮我修一下”。传统修图工具只能给你几个固定选项但Agent会自己判断这张图的主要问题是过暗其次是肤色偏黄、背景有轻微噪点。于是它会编排这样一个任务序列先调用“曝光校正”工具提高整体亮度再调用“白平衡”工具修复色偏然后调用“降噪”工具处理细节最后做一个整体色彩增强。整个链路不需要用户操作Agent自己规划、自己执行。这也是“Agent”和“普通功能模块”的本质区别普通模块是代码写死流程Agent是让模型根据输入动态决定流程。所以在这个项目里修图引擎反而是相对标准的部分真正复杂的是Agent的意图理解、工具选择、任务编排和异常处理机制。1.2 技术栈选型复盘Vue3 Golang Uniapp AI的组合逻辑这个技术栈不是拍脑袋定的每个环节都经过了实际考量。前端选择Vue3是因为Composition API在复杂交互场景下组织代码更方便而且配合TypeScript开发大型工作台类型约束能省掉不少低级错误。整个PC端界面包括图片上传区、任务列表、Agent执行日志面板、前后对比预览区组件通信比较多Vue3的响应式机制加上Pinia做状态管理整体开发体验比Vue2时代提升不少。移动端用Uniapp没有用纯原生或者React Native。原因很直接项目需要同时覆盖H5、微信小程序和Android/iOS App一套代码多端编译的收益太大了。Uniapp的API做了跨端统一封装比如图片上传这块在 Web 端用uni.uploadFile到小程序端也是同一套API虽然底层实现不同但业务代码不需要改动。这个项目里移动端定位是“轻量处理随时查看”核心操作路径就几个页面Uniapp完全够用。后端选Golang核心原因是两个词并发和部署。Agent在执行任务时需要同时处理多个图片处理请求Golang的goroutine天然适合这种IO密集型场景。另外Golang编译出来是单个二进制文件部署到服务器上不需要装Node环境、不需要弄Python虚拟环境一个二进制直接跑这对小型团队来说太友好了。AI层我们分了两个部分多模态大模型负责理解图片和用户指令图像处理引擎负责实际的像素级操作。前者是Agent的“大脑”后者是Agent的“手”。1.3 多端场景定义用户在哪里Agent就在哪里这段时间有一个很深的体会做多端项目最怕的是每个端做一套完全不同的产品逻辑那等于维护三个项目。我们最后定的原则是“一套Agent能力三端不同入口”。PC端是完整的工作台面向专业用户提供全部功能包括修图参数的高级调整、任务历史的详细查询、批量处理任务的管理。移动端只做轻量场景上传图片、发起修图指令、查看结果、保存分享。这个定位划分让我们可以控制移动端的开发量把精力集中在核心体验上。而且后端设计的时候所有接口天然就是给多端共用的。Agent的所有能力抽象成标准APIPC端调用和移动端调用走的是完全一样的链路只是入口不同。这也是全栈项目的一个核心思维后端能力要足够通用前端才能在不同端上灵活组织。2. Agent核心机制拆解2.1 从“一句话指令”到“可执行任务”意图识别与指令解析这个项目里Agent的第一步工作是把用户的自然语言指令转化成结构化的任务描述。比如用户说“把这张图调得通透一点”这句话里没有明确的参数Agent必须理解“通透”在图像处理上的含义然后把它转化为具体操作。我们在系统提示词System Prompt里给模型定义了清晰的输出结构要求它必须返回一个JSON数组每个元素包含三个字段action表示调用的工具名称params表示该工具的参数description用自然语言描述这一步在做什么。为了让输出更稳定我们还给了模型一份“工具使用手册”列出了每个工具的功能、适用场景和参数示例。实际的Prompt设计大概是这样的思路你是一个专业修图助手。用户会给你一张图片和一段自然语言指令。 请你分析图片内容拆解用户意图并选择需要使用的修图工具。 可选工具brightness亮度调整、contrast对比度、saturation饱和度、 white_balance白平衡、crop裁剪、denoise降噪、remove_object对象移除等。 要求 1. 输出格式为严格的JSON数组 2. 每个操作对象包含action、params、description三个字段 3. 参数必须符合工具定义的参数范围 4. 如果用户指令模糊根据图片内容做合理判断这个环节有个很关键的工程细节输出必须是严格的JSON格式但大模型的输出天然是自由文本很容易出现多余的说明文字。我们在模型端做了两重保障一是在系统提示词里强制规定“只输出JSON数组不要输出任何解释文字”二是在代码层做了解析兜底用正则截取第一个[和最后一个]之间的内容再解析如果还是解析失败就返回“指令无法识别”的提示让用户重新描述。2.2 工具注册表与Function Calling设计Agent能做什么取决于它手里有什么工具。我们设计了一个“工具注册表”模块所有的修图能力都以标准格式注册到Agent服务里。每个工具的描述包含工具名称、功能说明、参数Schema以及对应的内部实现函数。这里参考了业界常见的Function Calling设计模式但考虑到我们的场景相对固定没有直接用复杂的Agent框架而是自己实现了一个轻量级的工具调度器。每个工具在注册时声明自己的参数结构Agent拿到模型返回的操作列表后按照操作中的action字段去注册表查找对应的执行函数然后把params当作函数参数传入。工具注册的核心结构是这样的type ToolDefinition struct { Name string // 工具名称 Description string // 工具功能描述 ParamsSchema map[string]interface{} // 参数结构定义 HandlerFunc func(ctx context.Context, params map[string]interface{}) (*ToolResult, error) }这样做的好处是扩展性很强。以后要加一个新功能比如“背景替换”只需要写一个新的HandlerFunc注册进去然后在模型系统提示词的可用工具列表里加上这个工具Agent就自动获得了这个能力。工具层和Agent逻辑彻底解耦。2.3 任务状态机从Pending到Done的完整生命周期Agent执行任务不是一步瞬态操作而是一个完整的过程。我们需要让用户实时感知任务进展同时也要让系统在异常情况下有恢复和重试的可能。所以整个任务管理围绕一个状态机来设计。任务的生命周期分为这几个状态pending任务已创建等待Agent调度planningAgent正在分析图片和指令生成操作计划executing正在执行具体的修图操作completed所有操作执行完成图片已生成failed执行过程中出现无法恢复的错误cancelled用户主动取消任务每个阶段的状态变更都会记录一个时间戳和状态说明前端通过WebSocket实时接收这些状态变更从而在界面上展示完整的执行进度。用户看到的不只是一个“转圈等待”而是“Agent正在分析图片”“正在去掉背景物体”“正在调色”这样的详细步骤这种透明化的过程展示极大提升了产品的信任感。状态机用Golang实现核心就是一个带状态字段的结构体加一个控制并发安全的状态变更方法。Golang的并发模型在这里再次体现出优势sync.Mutex做状态互斥每一步操作通过context控制超时避免任务卡死在某个步骤上。2.4 异步任务与WebSocket实时推送修图操作尤其是对象移除、高分辨率降噪这类操作非常耗时可能从几秒到几十秒不等。如果直接用同步HTTP请求用户等个十几秒没有反馈体验很糟糕。所以整个任务链路设计成了异步模式。用户上传图片后前端先通过HTTP接口创建一个任务拿到任务ID然后通过WebSocket建立连接订阅这个任务的状态变化。后端收到任务后把任务丢进队列Agent调度器从队列中取任务按状态机逐步执行每个状态节点都通过WebSocket推送给订阅的前端。WebSocket在Golang这边用gorilla/websocket库实现维护一个连接池结构前端通过任务ID订阅对应的消息通道。这里有个细节不是每个任务维护一个独立的WebSocket连接而是所有前端共用一个连接消息里带上任务ID前端收到消息后按任务ID分发到对应的任务卡片上。这样避免了一个页面开多个WebSocket连接的资源浪费。3. 全栈实操过程核心模块的落地实现3.1 Golang后端Agent编排层与任务生命周期实现后端的核心是一个Agent编排服务我把它拆成了三个层次API层负责接收HTTP请求和管理WebSocket连接Service层负责业务逻辑和任务生命周期管理Executor层负责真正的工具调用和图像处理。任务执行的核心循环简化之后大概是这个逻辑func (a *AgentManager) ExecuteTask(ctx context.Context, task *Task) { // 更新状态分析中 task.UpdateStatus(StatusPlanning) // 第一步调用多模态模型生成操作计划 plan, err : a.parseUserIntent(ctx, task.ImageURL, task.UserPrompt) if err ! nil { task.Fail(fmt.Sprintf(意图解析失败: %v, err)) return } // 更新状态执行中 task.UpdateStatus(StatusExecuting) // 第二步逐步执行操作计划 for i, step : range plan.Steps { // 推送当前步骤进度 a.pushProgress(task.ID, StepEvent{ Step: i 1, TotalSteps: len(plan.Steps), Description: step.Description, }) // 查找工具并执行 tool, ok : a.toolRegistry.Get(step.Action) if !ok { task.Fail(fmt.Sprintf(未知工具: %s, step.Action)) return } result, err : tool.HandlerFunc(ctx, step.Params) if err ! nil { task.Fail(fmt.Sprintf(工具执行失败: %v, err)) return } // 把中间结果图更新到任务对象方便下一步操作 task.CurrentImageURL result.OutputURL } // 更新状态完成 task.Complete(task.CurrentImageURL) }这里“把中间结果图更新到任务对象”非常重要。因为修图操作往往是链式的前一步的输出是后一步的输入。比如“先调亮再降噪”降噪处理的目标是调亮后的图而不是原始图。所以整个执行链路上每个工具拿到的输入图片都要是当前最新的中间产物。有一个建议是给每个任务的图片处理设置超时和重试。图像处理服务在高并发下偶尔会超时一次性失败就终止整个任务太粗暴了。我们在Executor层加了重试机制每个步骤最多重试两次每次间隔3秒重试依然失败才标记任务失败。这样能明显提高任务成功率实测下来任务失败率从原来的8%降到了2%以下。3.2 图像处理服务的接入方式图像处理这块我们没有直接用大模型去逐像素修图而是把它拆成了独立的图像处理服务。原因很务实大模型直接改图一次调用成本高、耗时长而且参数控制不精确。我们更希望Agent的“大脑”负责决策而具体的图像操作交给专门的服务去执行。图像处理服务内部用了开源的图像处理库实现了各类基础操作能力亮度、对比度、饱和度调整、裁剪、白平衡、降噪还包括一些更高阶的功能比如对象移除。每个操作都以HTTP接口对外暴露接收图片URL和参数返回处理后图片的URL。对象移除这块用了实例分割相关的模型能力在服务端做推理。在项目初期这块推理速度很慢一张图要二三十秒。后来优化成先做低分辨率预览用户确认满意后再生成高清结果体验好了很多服务器压力也小了。图像处理服务部署在独立的GPU机器上和Golang编排服务通过内部HTTP通信。这里的网络开销其实是个瓶颈图片传过来传过去很占带宽。优化方案是把图片转成Base64直接在JSON里传简单场景下比传URL再拉取更快虽然Base64会膨胀约33%但省掉一次HTTP请求的往返时间。3.3 AI能力接入多模态识别与指令理解的设计细节多模态大模型在这套系统里扮演的是“决策大脑”它需要看懂图片内容理解用户用自然语言表达的意图然后给出可执行的操作方案。为了让模型输出稳定我们在Prompt设计上费了不少工夫。一个很重要的经验是把可能的用户意图枚举出来给模型做few-shot示例。比如我们在系统提示词里给了三四个典型场景的示例包括“人像逆光”“风景偏灰”“图片带杂物”每个示例都配了对应的JSON输出。模型有了参照输出质量明显更稳定不会随意发明不存在的参数。另一个细节是图片输入尺寸的控制。多模态模型的视觉编码器通常对输入图片有分辨率上限超过范围会被压缩导致细节丢失影响对图片内容的判断。我们的方案是给模型发送的图片统一预压缩到1024x1024以内的尺寸同时单独传一份图片的基础信息宽度、高度、文件大小让模型即使看不清细节也能根据元数据做基础判断。模型返回的JSON解析还有一招可以分享调用模型时把温度参数调到接近0。修图任务的可执行方案是相对明确的我们需要的是稳定输出不是创造性输出。温度调低后模型对JSON格式的遵循度明显提高极大地减少了解析失败的情况。3.4 Vue3前端Agent工作台的交互实现PC端工作台的开发量主要集中在三块图片上传与预览、任务进度展示、前后效果对比。这三块都用Vue3的组合式API封装成了独立组件。图片上传这块我们做了两个关键优化一是前端压缩用户选择的图片如果是几MB的大图先在前端用Canvas压缩到合适尺寸再上传大幅减少上传时间和服务端存储压力二是上传进度展示用XMLHttpRequest的upload.onprogress事件监听上传进度给用户一个直观的进度百分比避免感觉“卡住了”。任务进度展示是整个前端最具Agent特色的部分。用户可以实时看到Agent正在做什么比如“正在分析图片内容”“正在执行第2步去除杂物”。这个实时反馈就是靠WebSocket推送实现的。Vue3这边的状态管理用PiniaWebSocket收到消息后调用store里的action更新对应任务的状态字段Vue的响应式机制自动驱动UI刷新。前后效果对比组件也不复杂核心就是左右两张图加一条可拖动的分割线左边显示原图右边显示处理后的图。这里有个交互细节值得注意对比组件里最好用object-fit: contain而不是cover不然图片被裁切后用户对比起来会以为是效果问题。3.5 Uniapp多端适配的关键点Uniapp这套东西开发起来确实“一套代码多端运行”但真正做到多端体验一致还是有几个绕不开的适配点。第一是图片选择API的差异。H5端uni.chooseImage返回的临时文件路径可以直接用在image标签上但小程序端返回的临时路径有时效性如果用户隔了一段时间再发起修图这个路径可能就失效了。所以移动端的处理方案是拿到图片后立即上传到服务端后续所有操作都用服务端的URL不使用本地临时路径。第二是Canvas API的兼容性。如果要在前端做图片预览、裁剪或者压缩H5端的Canvas和微信小程序的Canvas在API上有不少差异尤其是canvas.getContext的调用方式。小程序端的Canvas需要指定type2d才能获取完整的2D上下文不指定的话很多方法不可用。这块写业务代码的时候要多写几层兼容判断或者直接用封装好的库。第三是样式的响应式适配。PC端的布局用flex和grid很自由但移动端需要针对不同屏幕尺寸做适配。我们最终定的方案是用rpx做单位Uniapp编译到不同端时会自动换算这套体系在小程序和App端的表现很好H5端也基本够用。字体大小和间距类样式全部用rpx避免在部分安卓机型上出现布局错乱。4. 常见问题与排查技巧实录4.1 前端三端不一致的兼容坑多端项目最磨人的就是“同一个页面这里正常那里错位”。实际开发中遇到最多的是H5端和小程序端的API行为差异。比如图片上传完成的回调回调参数结构不同H5端的res.data是字符串小程序端可能是对象需要先判断类型再处理。比较隐蔽的一个问题是H5端的WebSocket地址必须拼完整路径包括ws://协议前缀而小程序端有些情况下可以用相对路径。我们最后统一在配置管理里写了环境相关的变量不同端在编译时自动替换WebSocket地址彻底解决了这个隐患。4.2 Agent布局偶发卡死的定位方法项目测试阶段遇到过一个很头疼的问题Agent任务偶尔会卡在“逐步执行中”的状态既不成功也不失败任务队列越积越多。一开始以为是模型调用超时后来查了半天发现是Golang那边协程泄漏导致的。定位方法很有参考价值我们在配置里加了一个定时任务每隔30秒把当前所有任务的状态、关联的goroutine数量、WebSocket连接数打印到日志里。卡死问题复现后打开日志一看发现某个任务的goroutine数量一直在涨。顺着这个线索追下去发现是图像处理服务在超时边缘的时候主协程已经返回超时错误但处理响应的子协程还在等待写缓冲这个子协程没有设置写超时就永远卡住了。解决办法是给所有子协程的上下文都加上统一的超时控制同时WebSocket的写操作也设置写超时。这个问题的排查过程也再次说明全栈开发必须对整个调用链路的每个环节都有全局认识不然出了问题连从哪查都不知道。4.3 图片处理过慢与并发上限的调优经验上线初期经常有用户反馈“任务排队太久”查了一下是图像处理服务在高峰期单张处理时间明显变长因为多个任务同时打到GPU服务上显存不够就开始排队。优化方案分了两层第一层是任务队列的并发控制在Golang编排层限制同时执行的任务数超出部分的请求直接返回“队列已满请稍后重试”的提示至少不会无限堆积。第二层是图像处理服务内部做了分割线把处理任务按类型调度前处理耗时短的即时任务跳到前面耗时长的后台任务错峰执行。并发参数上我们按单卡最多同时跑2个推理任务来配置超过就排队等待。这个数值是根据GPU显存大小反复压测出来的调大之后单任务吞吐反而下降得不偿失。4.4 常见问题速查表现象可能原因解决方案任务卡在planning状态多模态模型接口超时增加接口超时时间加上重试机制超时后重新发起意图解析WebSocket时不时断开服务端没有做心跳检测前后端约定每30秒一个ping/pong心跳超时自动重连小程序上图片上传失败本地临时路径过期拿到图片后立即上传业务代码统一使用服务端URL处理结果图偏色色彩空间未转换图像处理服务统一使用sRGB色彩空间输出前做颜色配置转换高并发时任务大量失败图像处理服务被打满增加后端并发控制限制同时执行的任务数量同一张图多次处理结果不一致模型输出不稳定模型调用温度参数调低同时增加固定随机种子参数4.5 移动端内存溢出与长图处理还有一个只有在实际使用中才会发现的坑移动端处理长图比如截图、全景图时前端预览经常直接白屏严重的时候App直接闪退。原因是图片尺寸太大前端Canvas载入时内存爆了。解决思路是限制移动端处理的图片尺寸图片宽度超过2000像素就进行等比例压缩同时把处理任务完全交给后端前端只负责展示缩略图预览不加载原图到Canvas里处理。这样虽然损失了一部分原始精度但换来了稳定性和流畅度对移动端的定位来说是合理的取舍。5. 关于这个项目最后再聊几句这个项目从立项到完结给我最大的感触是全栈AI应用和传统的CRUD项目有本质区别。传统项目是流程驱动的用户点哪里代码就走哪里。但Agent项目是意图驱动的用户给一个模糊目标系统自己去拆解和决策。这种从“流程编写”到“能力编排”的思维转变是整个开发过程中最需要适应的地方。如果你也想做一个类似的Agent项目我的建议是先把工具层做实再去做Agent层的智能决策。工具不够稳定Agent编排得再花哨最后交付的结果也是不可靠的。反过来先把单一工具做到极高可靠度再叠加Agent的调度能力整个系统的稳定性就会有保障。至于技术选型Vue3加Golang加Uniapp这套组合在中小型Agent项目里确实很能打。Golang处理并发调度游刃有余Vue3组织复杂交互得心应手Uniapp让多端覆盖的成本降到最低。这套全栈技术路径大概率会陪伴我挺长一段时间。