
又一个新项目完结全栈 AI 修图 Agent这次不是普通的图像处理工具而是一个能听懂人话、自己拆任务、自己调用工具、还能不断自我修正的 Agent。简单说用户只需要在对话框里说“把这张图里的电线杆去掉顺便把天空调成黄昏色调”系统就会自动完成目标检测、区域修复、色调重映射这一整套操作。从需求拆解到最终部署前后端加 AI 推理链路全是一个人扛下来的所以这篇复盘会按我实际开发的顺序来写尽量把“为什么这样做”讲透而不是只贴代码。项目涉及的技术栈比较杂Vue3 TypeScript 做前端界面Go Gin 做后端聚合网关Python FastAPI 做 AI 模型推理服务再加上 uniapp 封了一层移动端壳。Agent 核心用的是大模型函数调用Function Calling图像处理部分踩了不少坑比如大图传输超时、模型显存溢出、Agent 多轮调用后上下文爆炸等等。如果你也在搞 AI 应用落地或者准备从普通 CRUD 转做全栈 AI 方向这篇东西应该能帮你省下不少试错时间。1. 需求拆解与整体设计1.1 这个 Agent 到底解决什么问题先想清楚一个事市面上现有的修图工具已经很强了PS 有 AI 填充手机相册有一键美化为什么还要自己做一个 Agent我当时的核心判断是——现在的修图软件还是“工具思维”不是“助理思维”。用户真实的诉求并不是“我要用套索工具选区域”而是“我想让这张图看起来更干净”。传统软件把修图拆成一堆参数和按钮用户得自己学习曲线Agent 则把自然语言直接映射成一系列图像操作。举个例子用户说“去掉背景里的路人”传统做法是手动抠图、修补、融合Agent 的做法是先检测人物、再区分主体和干扰项、对干扰区域做 inpaint、最后做边缘融合。这里面的每一步都有现成模型可以做但需要有一个大脑把任务拆好、排序好、执行好并且能在失败时自动重试。这个“大脑”就是 Agent 的核心价值。它的工作循环大概是理解用户意图 → 拆解子任务 → 调用图像处理工具 → 检查结果是否符合要求 → 不符合就调整参数再来一轮 → 直到用户满意或达到最大轮数。这跟用一个固定的 pipeline 处理图片有本质区别pipeline 是死的Agent 是活的。1.2 全栈技术选型思路定了“全栈”这个路线之后我首先纠结的是后端到底用 Node、Go 还是纯 Python。后来冷静下来想了几个实际约束前端和移动端需要快速复用代码uniapp 跑 Vue 语法是舒服的所以前端定了 Vue3。AI 推理免不了要跟 Python 生态打交道PyTorch、OpenCV、各种视觉模型基本都在 Python 侧所以 AI 服务必须独立出来。后端聚合层需要高性能、低内存、部署简单Go 在这方面的体验确实好编译完一个二进制扔服务器就能跑。任务可能需要异步化处理Go 写生产者消费者、连 Redis 队列都顺手。所以最终形成了“前端 Vue3 → 后端 Go 网关 → Python AI 服务”的三段式结构。中间用 HTTP 任务 ID 做异步状态轮询大文件走对象存储远端直传AI 服务内部再用进程内队列串行化 GPU 任务。为什么不直接让前端调 Python因为 Python 服务长期跑容易被各种内存问题拖垮放在 Go 后面比较稳而且以后可能接入多个 AI 服务换模型、加功能网关做统一鉴权和路由更方便。1.3 为什么做 Agent 而不是写死流程选型时我一度想走捷径既然用户指令无非就是去水印、抠图、调色、扩图这些固定类型那我搞一个意图分类器分到哪类就走哪个固定 pipeline 不就行了试到一半发现不行原因是真实用户的指令往往混合了多个操作。比如这句“把这个人抠出来放到海边背景里然后整体色调调成清晨的感觉。”这里面至少有抠图、背景替换、色调调整三个动作而且有先后依赖关系。如果只做分类很难处理组合场景。用 Agent 函数调用就自然得多先调segment_person拿到前景再调replace_background传入海景图再调color_grade传入“清晨”情绪描述。大模型天然擅长指令解析和动作编排这才是 Agent 比固定流程强的核心原因。还有一个附加收益Agent 在每轮工具调用后都能看到 tool 返回的结果摘要它可以根据结果决定下一步。比如抠图模型返回的掩码面积太低说明可能没检测到主体Agent 会自己改参数重试或者跟用户确认这种自愈能力是 pipeline 结构做不出来的。2. 核心细节解析与实操要点2.1 图像处理工具集不迷信单一模型Agent 再聪明手里没有好用的工具也是白搭。这个项目里我封装了 8 个核心工具全部以函数形式暴露给大模型工具名功能说明底层方案detect_object目标检测返回 bbox 和类别YOLOv8segment_person人像抠图返回 alpha 图RMBG-1.4 / MODNetremove_object指定区域内容移除与补全LaMa inpaintreplace_background背景替换抠图后融合color_grade色调迁移 / 风格化调色CLIP 颜色查找表super_resolution超分放大Real-ESRGANchange_format格式转换、压缩Pillow / sharpcompare_quality图像质量评估与对比手工规则 CLIP 相似度选模型时不是越重越好比如抠图如果直接用 SAMSegment Anything显存占用高不说推理速度在 CPU 环境根本扛不住。实际线上我默认用 MODNet 做轻量人像分割只有在用户明确要求“精细到头发丝”时才切换到 RMBG 大模型。LaMa inpaint 的效果确实惊艳移除物体后背景结构保持得很好但它对大分辨率图会比较慢所以我在它前面会加一步先对移除区域周边做裁剪修复完再贴回去这样速度和内存都能省一半。2.2 Agent 对话循环与函数调用实现Agent 的底层逻辑其实不复杂关键是把循环写稳。我用的是 LLM 的 function calling 能力每次请求都会把当前可用的工具列表、用户最新指令、历史对话记录一起塞给模型。模型输出要么是普通文本回复要么是结构化的工具调用请求比如{name: remove_object, arguments: {bbox: [152, 34, 88, 120]}}。我实现的简化步骤如下用户上传图片并输入文本指令前端把图片压缩到宽边不超过 2048同时保留原图 URL。后端收到请求后创建任务生成task_id把图片先落到对象存储。进入 Agent 循环把工具定义、图片 URL、用户指令拼成 system user 消息发给大模型。模型返回工具调用时执行对应 Python 工具函数并把结果摘要比如“removed object successfully耗时1.2s输出图URL是xxx”以 tool 消息回传给模型。模型判断任务是否完成如果完成就输出最终文字说明和结果图 URL否则进行下一轮。设置最大循环次数我设的是 6 轮防止 Agent 陷入死循环浪费 token。这里有几个关键参数值得关注temperature 我固定为 0.1让模型尽量少自由发挥工具描述必须写得极其清楚包括参数含义、取值范围、什么情况不该调用上下文里塞的图片不是原图而是一个缩略图 base64因为大模型只能理解图像语义不能直接理解像素级问题——这一步很多人会忽略。2.3 大图传输与并发处理性能瓶颈怎么破图像处理项目最容易翻车的不是模型精度而是图片太大导致链路超时。我一开始直接用 Base64 把图片从 Go 网关传给 Python 服务结果一张 5MB 的图光编码传输就花了十几秒前端早超时了。后来整体改成三步走前端先做一次压缩预览上传原图到对象存储得到source_url。Go 网关只传 URL 和任务参数给 PythonPython 用requests或curl从对象存储拉图。处理完的结果图同样落回对象存储Python 只回传结果 URL 和元信息。这个改动把链路的传输压力彻底转移到了对象存储带宽上Python 服务本身的 I/O 负担小了很多。性能数据也挺明显原来 5MB 图的端到端耗时约 40 秒现在压缩后处理耗时能控制在 8 秒左右包含模型推理。并发方面GPU 显存是硬约束。我的 8G 显存卡只能同时跑一个中等模型如果并发任务多所有请求都会挤在一起显存必然爆。解决方式是在 Python 服务里加了一个带超时的进程内信号量同时用 Redis 做任务队列Go 网关接到请求后把任务塞进队列就返回processing状态前端轮询结果即可。这样接口不会因为 AI 推理慢而阻塞用户的体验也更好。3. 实操过程与核心环节实现3.1 前端交互层预览、对比与多端适配前端看起来不起眼但决定用户愿不愿意用。这个项目里我花了将近三分之一的时间在前端界面上核心痛点有三个图片编辑过程必须所见即所得、前后效果要能一键对比、同套代码要能跑在手机端。技术选型是 Vue3 TypeScript Pinia。图片编辑区域用的是 canvas 实现监听鼠标拖拽绘制修复区域绘制结果同步转换成 bbox 传给后端。这里要特别说一下 canvas 的坐标换算问题用户在屏幕上的绘制坐标是 CSS 像素而传给后端的 bbox 必须换算成原图像素坐标。如果图片被缩放显示需要在鼠标事件里除以当前缩放比例否则修复区域就会偏到天边去。多端复用我直接用 uniapp 包了同一套 Vue3 代码UI 层用自适应布局处理不同屏幕宽度。虽然有部分原生 API 差异但因为核心逻辑都在后端前端只负责展示、上传和指令收集所以移动端适配成本可控。实测下来 iOS 和 Android 的 H5 壳都能正常跑只是大图预览时内存占用偏高需要做 canvas 缩放和内存释放。3.2 后端聚合服务任务编排与排队逻辑Go 网关的职责很集中鉴权、接收文件、创建任务、轮询状态、转发结果。路由结构大概是r.POST(/api/v1/task, handler.CreateTask) // 创建任务上传图片指令 r.GET(/api/v1/task/:id, handler.GetTask) // 查询任务状态和结果 r.GET(/api/v1/history, handler.GetHistory) // 历史记录CreateTask里我做了几件关键事先校验上传文件类型和大小超过 15MB 直接拒绝图片落对象存储时文件名用 UUID防止重复和路径穿越然后把task_id写入 Redis 队列MySQL 记录任务详情最后返回task_id给前端。任务状态我用一个枚举管理pending、processing、success、failed。前端每 1.5 秒轮询一次/api/v1/task/:id如果拿到success就展示结果图和对比图如果failed就把错误信息直接弹给用户。最开始我试过用 WebSocket 推结果但后来觉得轮询反而更简单可靠而且服务端不用维护长连接状态少了一堆资源泄漏的隐患。排队逻辑也放在 Go 层Redis 里用一个counter记录当前正在执行的任务数超过阈值比如 2就返回一个重试提示给前端让用户稍后再试。这种“软限流”虽然粗暴但对于个人项目或小团队来说比引入复杂的高可用架构更务实。3.3 AI 推理服务模型加载、批处理与内存管理Python 服务的核心文件结构我大概分成了这四块ai_service/ ├── main.py # FastAPI 入口路由和任务分发 ├── agent.py # Agent 循环逻辑工具调用管理 ├── tools/ │ ├── __init__.py # 工具注册表统一暴露给 LLM │ ├── inpaint.py # 内容移除类工具 │ ├── segment.py # 抠图类工具 │ ├── color.py # 色调调整类工具 │ └── utils.py # 图像加载、保存、质量评估 ├── models.py # 模型加载管理器模型管理这个环节有个坑如果每次请求都在函数内部torch.load()加载模型显存会反复申请释放页面一卡机器就崩。我的做法是在启动时把模型加载成全局单例按需初始化一个“模型池”_model_registry {} def get_model(name): if name not in _model_registry: _model_registry[name] load_model(name) return _model_registry[name]这里要特别注意的是不同模型占用的显存不可控池子里的模型越多单个模型能用的显存就越少。我的实践是小型模型MODNet、LaMa、YOLOv8常驻大模型Real-ESRGAN、RMBG按需加载使用后立即释放。策略是“模型优先级 LRU淘汰”长期不用的模型先从显存卸载要用时再加载保证主链路不会被突发的大任务打死。agent.py里的循环逻辑我做了不少细节优化其中最重要的一个是每次工具调用后返回给 LLM 的摘要不要包含整个图片 base64只给缩略图、耗时、关键指标。这样能控制上下文体积减轻大模型的认知负担和 token 开销。我实际测过如果每轮都把处理后的全图塞给 LLM不出三轮上下文就能冲爆窗口上限。另外我还给每个工具加了超时控制默认 30 秒。比如remove_object在长宽都超过 2000 像素的大图上LaMa 可能要跑 40 秒以上此时需要提前裁剪处理区域而不是让整个请求卡死。代码里直接判断如果 bbox 对角线长度超过图片的 40%先缩放到 1024 内跑再放大回原分辨率做贴回。3.4 联调与性能验证从 40 秒压到 8 秒整个链路完成后我做了一轮压测用的是 10 张不同分辨率的图片从 720p 到 4K每张都跑“去水印”“抠图换背景”“色调调整”三个典型任务。结果如下场景优化前耗时优化后耗时说明上传 创建任务1.8s0.4s主要优化点压缩上传、直传对象存储Agent 解析指令1.2s0.8s减少历史轮次精简 prompt抠图720p4.5s1.6sMODNet 轻量模型常驻不走重载内容修复720p6.8s3.1s区域裁剪处理后再贴回色调调整720p2.2s1.9sCLIP 特征预计算后缓存全链路1 张 720p 图16.5s7.8s包含对象存储传输、结果返回这个数据足够说明问题优化链路比堆机器靠谱得多。我把 Agent 的每轮工具调用耗时记录下来发现最慢的步骤永远是图像模型推理而不是大模型的文本生成。所以性能优化的优先级应该是“推理速度 传输大小 排队调度 代码细节”。4. 常见问题与排查技巧实录4.1 大图传输超时问题现象上传一张原始分辨率 4000x3000 的手机照片前端始终报“请求超时”。排查过程看日志发现请求在对象存储上传阶段就花了 20 多秒。原来对象存储直传的签名字段里带了大量参数前端浏览器对同一连接有并发限制导致跟图片压缩接口相互阻塞。解决方案调整为前端先压缩图片限制宽边 2048、质量 0.85再把压缩图上传获得preview_url原图作为source_url也上传但置为异步后台任务。Agent 优先用preview_url做语义理解实际做高精度编辑时才拉取source_url。实测 4K 原图的端到端延迟从 30 秒降到 9 秒左右。经验图像类 AI 产品前端永远不要直接传原图给后端推理先压缩、再缩放、必要时候再传原图这条策略能解决 50% 以上的超时问题。4.2 Agent 上下文爆炸和无效工具调用现象连续对话 10 轮以上后成本暴涨而且模型会重复调用同一个工具比如用户说“再清晰一点”Agent 反复调super_resolution三四次结果图越来越假。原因上下文里的图像缩略图太多模型视觉注意力被分散无法有效判断“已经达到要求了”。解决方案历史记录里只保留最近两轮的内容之前的工具结果替换成一句话摘要给super_resolution工具增加了“幂等”说明如果图像清晰度评分超过阈值工具会直接返回noop避免无意义放大。另外我还给 Agent 设了一个对话记忆池只把与当前任务相关的指令放进 prompt其余信息放向量库记忆按需检索。经验Agent 的“任务完成判停”比“任务执行”难十倍必须用工具自反馈 阈值判断来阻止模型发疯不能完全靠 prompt 约束。4.3 模型显存溢出 OOM现象同时来了两个“超分”任务Python 服务直接退出日志显示CUDA out of memory。原因Real-ESRGAN 是重模型默认加载后占显存 4G 多另一个任务再用就爆了。我原来是每请求都加载模型后来改成全局单例但没控制并发。解决方案加了一个torch.cuda.synchronize()delgc.collect()的显存回收逻辑同时用信号量限制同时执行的模型任务数sem threading.Semaphore(1) # 同一时刻只允许一个推理任务 def run_inference(model, *args): with sem: return model(*args)这里显存回收不是即时的PyTorch 有自己的 caching allocator所以“任务结束但显存没归零”非常正常。我用的土办法是任务完成后调用torch.cuda.empty_cache()配合一个定时任务每 5 分钟检查显存占用超过阈值就手动清一次。对于个人项目这种方案已经足够稳。经验GPU 显存是稀缺资源任何模型池方案都建议配上“可用显存上报”接口让 Agent 层在低压时再启用重模型避免 OOM 导致整个服务不可用。4.4 图像质量损失严重现象图片经过多次处理比如先抠图、再调色、最后超分后边缘出现白边色彩也偏灰。原因每次工具调用都做了 JPEG 压缩重存经过三轮有损压缩质量自然垮掉。解决方案把中间结果统一保存为 PNG无损格式只有最终输出时才转成 JPG。工具内部处理时全程保持 RGB 三通道抠图时额外保存 alpha 通道背景替换时再做 alpha 融合避免传统“黑底合成”造成发丝边缘溢出。经验图像链路设计务必把“中间格式”和“最终格式”分开中间一律无损最终转码一次。这个原则每一条图像处理管道都适用。4.5 多端适配怪异问题现象uniapp 打包的 H5 在电脑端一切正常在安卓微信浏览器里上传图片后canvas 绘制坐标偏移严重画出来的修复区域跟手指点按的位置差了几十像素。原因微信内置浏览器的历史版本不支持touch事件与鼠标事件完全等价部分元素默认有 touch 滚动行为导致在 canvas 上的touchmove坐标被浏览器拦截或补偿。解决方案监听pointerdown、pointermove而不是touchstart、touchmove再加上touch-action: noneCSS 属性禁用默认手势。这一改安卓、iOS 和桌面端都统一了。经验多端优先用 Pointer Events API不要为不同端分别写一套事件监听维护成本高且容易遗漏。5. 项目复盘与可复用的经验5.1 Agent 项目的架构心得做完这个项目我个人最大的感受是Agent 没那么玄乎本质上还是一个“理解—决策—执行—反思”的循环。真正的工作量主要在两块一个是工具的质量与稳定性另一个是循环的终止条件设计。很多人一上来就调大模型 prompt希望模型“表现得聪明一点”但效果往往不稳定。我亲测下来把更多精力花在工具封装上更划算如果remove_object的修复效果足够好、边界处理足够干净那么 Agent 只需要对工具输出做一次质量检查就可以直接交付。工具不稳Agent 再怎么调整对话策略也是空中楼阁。“终止条件”同样重要。这个项目里每个工具都可以返回一个质量评分比如超分后的边缘清晰度、抠图后的 alpha 置信度、调色后的整体色彩直方图差异。Agent 拿到这些评分后做比较一旦达到“用户指令的目标阈值”就停止迭代并输出结果。这种结构化反馈远比让模型自己看缩略图靠谱成本低、可控性强。5.2 全栈工程师怎么把控项目边界一个全栈项目最怕的就是什么都想自己写结果每个环节都浅尝辄止。我的经验是优先选用成熟稳定的底层能力把精力集中在产品逻辑编排上。比如目标检测直接用了 YOLOv8 的官方权重没有自己去训练专属模型抠图用了学术界验证过的 MODNet图像修复用了 LaMa 的预训练 checkpoint。这些模型效果已经足够支撑产品演示和中小用户量使用省下的时间全花在了 Agent 编排、前后端联调、性能优化这些真正影响用户体验的地方。还有一点全栈项目一定要尽早确定接口协议。我是先定了一套简单的 JSON 协议task_id status result_url前后端并行开发等两边都写好了再联调效率高很多。如果一开始就纠结谁先谁后很容易在联调阶段返工。5.3 后续可以怎么扩展这个 Agent 目前还只能处理单图任务后续我觉得有两个方向可以继续深挖一是做成批量处理工作流比如一次性导入 100 张商品图用自然语言描述统一的处理规则跑完一整套自动化流程这是电商场景的真实需求二是把 Agent 的记忆和策略模块独立出来让它能在不同图像处理工具之间做自适应调度比如根据用户历史偏好推荐不同的修图风格。另外一个很值得做的是“结果对比与用户反馈回路”每次用户对结果满意或不满意的点击行为都当成训练数据收集起来后续可以做模型的微调或者 Agent 策略的强化学习。这块我还没做扎实但对一个全栈 AI 应用来说反馈数据是最核心的资产。最后再分享一个我在实际项目中反复踩的坑不管 Agent 吹得多智能用户上传的图片格式千奇百怪HEIC、WebP、RGBA、CMYK 全都可能遇到。一定要在入口统一处理图像解码和颜色空间转换转成 sRGB不然后面所有模型都会出现各种诡异结果。这条我每次做图像项目都会写进架构清单第一位。项目做到这里功能闭环已经完成从用户输入自然语言到拿到一张满意的修图结果整个链路都是通的。以后再有图像类需求我大概率会直接复用这套“Agent 编排 工具注册 异步任务”的骨架把核心精力放在具体模型的迭代和新场景的适配上去。