当前位置: 首页 > news >正文

我让 3 个子 Agent 同时改同一个文件,没打架——因为偷了 Git 的一个冷门功能

文章目录

    • 引子
    • 一、原来的子 Agent 为什么是"假并行"
    • 二、怎么改成真正并行
    • 三、只读类型无限制并行,GENERAL 怎么办
    • 四、Git Worktree:冷门但正好解决这个问题的功能
    • 五、ThreadLocal:一句话让 FileTool 指向正确的 worktree
    • 六、线程安全全景
    • 七、Worktree 失败怎么办
    • 总结

引子

你让 AI 同时分析 SQL 注入和 XSS 漏洞。它启动了 2 个子 Agent——然后第二个干等了 30 秒

因为代码里SubagentRunner.run()是同步阻塞的。

我花了一下午把子 Agent 从排队改成并行,又用 Git 的一个冷门功能解决了多 Agent 同时写文件的冲突。整个过程比我想象的顺利得多——因为架构一开始就把线程安全考虑进去了。


一、原来的子 Agent 为什么是"假并行"

先看原来的代码:

// TaskTool.execute() — 第 77 行Stringresult=SubagentRunner.run(ai,dispatcher,registry,config,prompt,maxRounds,type);returnToolResult.success(result);

SubagentRunner.run()内部是一个for循环,最多跑 10 轮 LLM 调用。这个方法不返回,父 Agent 就没法往下走。

即便父 LLM 一次返回了 3 个task()调用,AgentLoop 的处理逻辑也是:

for each tool call: tools.execute(req) ← task() 在这里面卡住了 第二个 task() 要等第一个跑完才能进循环

本质:串行排队,不是并行。


二、怎么改成真正并行

新建一个SubagentManager,核心就一个固定线程池:

privatefinalExecutorServicepool=Executors.newFixedThreadPool(4);

submit()方法把子 Agent 提交到线程池,立刻返回任务 ID

publicStringsubmit(...){Stringid=UUID.randomUUID().toString().substring(0,8);pool.submit(()->{Stringresult=SubagentRunner.run(...);// 在线程池的线程里跑notifications.add(完成通知);// 跑完塞进队列});returnid;// 立刻返回,不等待}

TaskTool.execute()从同步等结果变成:

Stringid=subagentManager.submit(...);returnToolResult.success("子 Agent ["+id+"] 已启动,完成后自动通知。");

父 Agent 的循环每轮调drain()扫一遍通知队列,有结果就注入对话历史:

父 LLM 调用 task() → "已启动 [a1]" 下一轮 drain() → "子 Agent [a1] 完成: 发现 3 处 SQL 注入..." 父 LLM 看到结果 → 决定下一步

效果:

父 Agent 不阻塞。同一轮 LLM 返回 3 个 task(),三个子 Agent 同时启动。


三、只读类型无限制并行,GENERAL 怎么办

问题来了:EXPLORE/PLAN/VERIFICATION这三种只读的,10 个同时跑都没事。但GENERAL是会写文件的:

GENERAL-A: file(write, "UserService.java", "版本A") GENERAL-B: file(write, "UserService.java", "版本B") → 后写的覆盖先写的,A 的修改静默丢失

这就是为什么要用Git worktree


四、Git Worktree:冷门但正好解决这个问题的功能

git worktree能在一个仓库里创建多个独立的工作目录

git worktree add --detach /tmp/agent-worktrees/wt-a1

这条命令干了什么:

  • /tmp/agent-worktrees/wt-a1创建一个完整的项目副本
  • --detach表示不创建新分支,用 detached HEAD 模式
  • 这个目录完全独立——有自己的文件、自己的.git元数据
  • 创建速度很快(秒级),因为它共享底层 git 对象,不是cp -r

每个需要写文件的 GENERAL 子 Agent,启动前先拿一个独立 worktree:

GENERAL-A: /tmp/agent-worktrees/wt-a1/UserService.java ── 写这里 GENERAL-B: /tmp/agent-worktrees/wt-b2/UserService.java ── 写这里 父 Agent: ./UserService.java ── 原文件不动

三个互不干扰。改同一个文件也不会互相覆盖。

子 Agent 完成后,通过git diff把修改拉回来:

Stringdiff=worktreeManager.getDiff(worktreePath);// git -C /tmp/wt-a1 diff → 所有文件改动

父 LLM 在通知里看到完整的 git diff,由它决定要不要把改动合入主分支。子 Agent 只管"改",父 Agent 负责"判"。


五、ThreadLocal:一句话让 FileTool 指向正确的 worktree

worktree 有了,但FileTool有个大问题——它的工作目录是静态字段

// FileTool.javaprivatestaticPathWORKSPACE;// 启动时设置,之后不变

所有线程都共享这一个WORKSPACE。子 Agent 在 worktree 里调file(write, "UserService.java", ...),怎么保证它写到/tmp/wt-a1/而不是主目录?

加一行 ThreadLocal:

privatestaticfinalThreadLocal<Path>WORKSPACE_OVERRIDE=newThreadLocal<>();// 子 Agent 启动前FileTool.setWorkspaceOverride(worktreePath);// 当前线程 → worktree// safePath() 里Pathworkspace=WORKSPACE_OVERRIDE.get();if(workspace==null)workspace=WORKSPACE;// 没人设就用默认// 子 Agent 结束后(finally 块)FileTool.clearWorkspaceOverride();// 当前线程恢复

为什么 ThreadLocal 而不是传参?

因为FileTool的调用链路是:LLM → ToolDispatcher → FileTool.execute()。中间没有任何地方让你"顺便传一个 workspace 进去"。ThreadLocal 是唯一不破坏现有架构的注入方式。

线程安全保证:ThreadLocal 的 set 只影响当前线程,线程 1 设了/tmp/wt-a1不影响线程 2 的/tmp/wt-b2。比锁优雅得多。


六、线程安全全景

整个子 Agent 系统从头到尾不需要一把锁,原因:

资源安全机制
SubagentRunner内部状态全局部变量,每次调用 new 一份
AIService(API 调用)无状态 HTTP 客户端,天然线程安全
ToolDispatcher / ToolRegistrywithout()返回新副本,不共享
FileToolworkspaceThreadLocal线程级隔离
通知队列ConcurrentLinkedQueue
运行任务跟踪ConcurrentHashMap + AtomicInteger

唯一的共享是 DeepSeek API Key——但两个 HTTP 请求之间不存在竞争,就像两台电脑用同一个 WiFi。


七、Worktree 失败怎么办

worktree 可能创建失败——不是 git 仓库、有未提交的修改、磁盘满了。GENERAL 子 Agent 拿不到 worktree 时,不降级到主 workspace

if(worktreePath==null){// 直接报错,不跑子 Agentnotifications.add(错误通知:"git worktree 创建失败,请检查...");return;}

父 LLM 看到错误后有三种选择:

  1. bash("git status")检查状态,修好后重试
  2. 改用task(type="explore")做只读分析
  3. 自己动手改,不用子 Agent

关键原则:写操作拿不到隔离环境就拒绝执行。宁可不干活,不能瞎干活。


总结

  1. 异步不阻塞— 子 Agent 提交到线程池后立刻返回,父 Agent 继续循环
  2. 只读无限制并行— Explore/Plan/Verification 纯读不冲突
  3. GENERAL Worktree 隔离— 每个写操作子 Agent 跑在独立 git worktree 里
  4. ThreadLocal 防串— 一句话让 FileTool 指向正确目录,线程级隔离
  5. 失败不降级— worktree 拿不到就报错,绝不动主 workspace

整轮改造 4 个新文件 + 6 个修改文件,核心改动不到 300 行。

点赞收藏,面试能讲 10 分钟。


http://www.gsyq.cn/news/1429195.html

相关文章:

  • 法律AI工具选型终极决策矩阵(含22家供应商穿透式测评+17项等保2.0/《人工智能法(草案)》适配度评分)
  • Arduino自制反应计时器:从硬件搭建到数据分析全流程
  • 【系统学AI】14 RAG工程实践(2026版):从0到生产的全栈技术选型
  • 2026 苏州黄金回收避坑指南!4 家通过 10 项核查店铺推荐+口碑实测汇总 - 资讯纵览
  • AI技术热点简报 | 2026.05.29
  • 终极指南:3分钟快速激活Windows和Office的完整解决方案
  • Kazumi WebDAV同步功能终极指南:3步实现跨设备番剧数据互通
  • 您的岗位情报官上线,ArkClaw「每日情报助手」带您吃透全行业
  • 项目实战:中风数据分析
  • 洛阳市中央空调维修师傅推荐|全城各区金牌师傅,靠谱选欧米到家 - 欧米到家
  • 【紧急更新】2024春招已启用新一代AI简历筛查引擎:你的ChatGPT求职信正在被自动降权(附3分钟急救校验清单)
  • 如何用Scarab为《空洞骑士》打造智能模组管理生态:3大核心机制深度解析
  • 基于ESP32与Firebase的智能安防系统:从硬件到云端的物联网实战
  • 5.30 合肥黄金回收,今日大盘附近正常报价 - 资讯纵览
  • 2026年C++最热实测(二)——C++26那些“不起眼”却救命的新特性
  • 【紧急预警】传统知识库系统将在18个月内集体失效:AI原生知识管理迁移倒计时启动(含兼容性评估工具包)
  • 基于ESP32与LVGL的嵌入式GUI开发:圣诞雪花球交互项目全解析
  • TypeScript高级特性:提升代码质量
  • Gemini数据分析报告生成逻辑首度公开:基于217份企业级报告的逆向工程分析(限期内部资料)
  • Ovito 3.6.0基础版也能搞定:手把手教你用CNA和W-S法可视化辐照损伤中的晶界与点缺陷
  • 3分钟掌握Sketch批量重命名:告别混乱图层管理的终极指南
  • 【限时解密】:某Top3律所内部使用的Claude文档推理增强框架(含OCR对齐校验模块源码片段)
  • Anthropic深夜炸场,最强旗舰 Claude Opus 4.8 发布,代码与Agent能力全面进化!
  • 销售团队为什么需要 CRM 与合同、回款、审批联动
  • Finalshell连接报错‘Connection refused’?可能是你虚拟机SSH配置的这处细节没改
  • Arduino步进电机与RGB LED协同控制:打造智能旋转发光花
  • AI工具版权雷区地图(含GitHub Copilot、Runway、Jasper等12款主流工具实测结论):你的公司正在踩中哪一条?
  • 2026年AI智能写作排行榜:实战应用指南与顶级工具推荐
  • 事件相机与合成数据技术解析与应用
  • SAP MM采购订单实操:K成本中心消耗品采购,从ME21N到MIGO的完整流程与FI凭证解析