1. 为什么我会想到用 AI Agent 来驱动 Unity 编译与测试
这事的起因其实挺朴素——项目迭代节奏快了之后,人力编译、打包、跑测试这套流程开始变成瓶颈。
那时我们组的日常是这样的:程序改完代码,先在编辑器里等编译,然后手动切到 Test Runner 跑一轮 EditMode 测试,再看结果。如果涉及安卓或微信小游戏目标平台,还得先切 Build Target,再执行 Build,每次构建十几分钟起步。一天下来,真正写逻辑的时间被这些机械操作吃掉了不少。更要命的是,这类操作高度流程化、状态依赖强,人一旦中途被别的事情打断,很容易漏掉某一步,比如忘了切回 Editor 平台,或者跑完测试忘了看失败的具体断言。
后来我们接触到 AI Agent 的概念——准确说是具备工具调用能力的 Agent,它能理解自然语言指令,调用一堆预定好的工具来完成任务。我当时就想,编译、跑测试这类操作,本质上就是“有明确输入、有确定步骤、有可预期输出”的工具调用场景,非常契合 Agent 的执行模型。于是就有了这篇文章要聊的那个项目:把 Unity 编辑器本身改造成 AI Agent 的“技能终端”,让 Agent 通过命令行驱动 Unity 的批处理模式,完成编译、测试、生成测试报告,再把结果回传给 Agent 做分析和决策。
先说结论:这个方向完全可行,而且踩完坑之后,整体效率提升非常明显。但过程中也遇到了不少“文档里根本不会写”的问题,比如 Win 平台下批处理输出的编码问题、批处理模式下 Editor 状态残留导致的“幽灵失败”、测试报告 XML 解析的坑等等。这篇文章我按实际推进的顺序,把整个工具链的搭建、修复和验证过程完整记录一遍。
适合谁来读?两类人:一是 Unity 项目里负责搭 CI/CD、搞自动化的开发者,二是正在研究 AI Agent 落地、想给 Agent 接真实开发工具链的技术人员。你不需要对两者都精通,我尽量把 Agent 部分和 Unity 部分的关键原理都拆开讲清楚。
2. 整体架构设计:Agent 并不“直接”碰 Unity,而是通过命令桥接
2.1 为什么不能指望 Agent 直接操作 Unity 编辑器 UI
最早有人提过一个方案:给 Agent 装个屏幕控制工具,让它像人一样打开 Unity 编辑器,点击菜单栏,操作 Test Runner 窗口。我第一时间否掉这个方案。
原因很现实,Unity 编辑器是一个 GUI 应用,UI 控件布局、菜单路径、弹窗行为会随版本变化,而且存在大量非确定性因素——比如某个对话框是不是弹出来了、Asset 导入进度条走到哪了、Library 目录是否正在被占用。Agent 靠“看屏幕”来理解这些状态,误判率极高,出了问题还非常难调试。
更合理的做法是把 Unity 当做一个“无头命令行程序”来驱动。其实 Unity 本身就内置了完善的批处理模式(BatchMode),命令行参数可以直接让它打开指定项目、执行指定静态方法、跑测试、构建产物,然后退出。这和人类点鼠标的路径完全不同,但结果一致,而且状态可控。
于是架构变成了这样:
- AI Agent 收到用户指令,例如“重新编译一下项目并跑一下 EditMode 测试,失败了告诉我失败原因”
- Agent 在它的工具列表里找到
unity_run_tests这个工具 - Agent 调用该工具,实际上是执行一个预先写好 CLI 脚本
- CLI 脚本内部拼装 Unity 命令行参数,调用 Unity 批处理模式
- Unity 执行编译或测试,把结果写入日志文件和 XML 报告
- CLI 脚本解析结果,输出一段精简的结构化文本给 Agent
- Agent 根据这段文本决定下一步动作——是报告成功、还是阅读具体失败日志、还是修复代码后重跑
这套设计的核心原则是:Agent 永远通过“封闭工具”和外部环境交互。工具来保证操作的安全性和确定性,Agent 只负责理解和决策。
2.2 CLI 工具层要封装哪些能力
我这个项目的 CLI 工具层一共封装了四个核心操作,全部用 Python 写的(也可以换成 Node 或 Go,选 Python 纯粹是团队熟悉):
| 工具名 | 作用 | 典型触发场景 |
|---|---|---|
unity_compile | 强制触发脚本编译并输出编译错误列表 | “编译一下” |
unity_test_editmode | 运行全部或指定规则的 EditMode 测试 | “跑一下单元测试” |
unity_test_playmode | 运行 PlayMode 测试(会进 play 模式) | “跑一下集成测试” |
unity_report_latest | 读取最近一次测试的输出报告并整理成摘要 | “测试结果怎么样” |
这四层对应到 Unity 侧的静态方法,每一个方法都挂在同一个 Editor 脚本里,通过-executeMethod触发。记住:Unity 的-executeMethod只接受静态方法,而且方法所在类必须放在Editor文件夹下,否则打包和运行时会报方法找不到。
2.3 关键命令行参数解析
这里直接贴一份我实际使用的命令模板,标注了每个参数的用途。以 EditMode 测试为例:
$UNITY_PATH \ -batchmode \ -nographics \ -projectPath "D:\Projects\MyGame" \ -runTests \ -testPlatform EditMode \ -testFilter "MyGame.UnitTests" \ -testResults "D:\Projects\MyGame\TestResults\editmode-results.xml" \ -logFile "D:\Projects\MyGame\TestResults\editmode-run.log" \ -quit逐个解释一下:
-batchmode:进入批处理模式,不显示编辑器窗口,不接受玩家输入。这是 Agent 驱动的必备前提。-nographics:禁用图形设备初始化。跑 EditMode 测试一般用不上渲染,加上它能避免在某些无显卡的 CI 机器上启动失败。-runTests:告诉 Unity 这次启动为执行测试。-testPlatform EditMode:指定测试平台。注意这里的值严格区分大小写,写editmode会直接报“未知测试平台”。-testFilter:测试过滤规则,支持命名空间、类名或方法名。如果不加,则运行全部测试,速度会很慢。-testResults:测试结果 XML 输出路径。-logFile:Unity 运行日志路径。这个参数在 Windows 下特别重要——不指定的话,批处理模式下日志会写入用户目录的AppData\Local\Unity\Editor\Editor.log,Agent 找起来费劲,多个进程同时跑还会互相覆盖。
-quit:所有命令执行完后退出编辑器。这一步很容易被忽略,实际不加它也能退出,因为-batchmode下 Unity 会默认执行完就退出,但显式加上更保险,语义也更清楚。
编译的调用方式则是这样的:
$UNITY_PATH \ -batchmode \ -nographics \ -projectPath "D:\Projects\MyGame" \ -executeMethod "EditorToolchain.CompileProject" \ -logFile "D:\Projects\MyGame\TestResults\compile-run.log" \ -quit其中EditorToolchain.CompileProject是我们在 Editor 脚本里定义的静态方法,里面调用CompilationPipeline.CompilePlayerScripts()来触发编译。这个方法在 Unity 2020.3 之后是公共 API,以前是内部方法,网上很多老教程用的是反射,现在已经不需要了。
3. 第一个大坑:批处理模式下 Editor 状态残留导致的“幽灵失败”
3.1 现象描述
工具链第一版搭好之后,我做了个简单验证:故意在一个测试方法里写了个断言失败的用例,然后让 Agent 去跑测试。结果一切正常,测试报告里明确标注了失败。
接着我把这个断言失败的用例改回正确逻辑,再次让 Agent 跑。诡异的事情出现了——测试报告里仍然显示失败,而且报错内容、堆栈还有行号,都跟上一个版本一模一样。
我当时的第一反应是编译没生效。但问题在于,unity_compile是单独一步,Agent 先调用它,等返回成功后,才调用unity_test_editmode。编译日志里也明确显示脚本编译已完成。那唯一能解释的就是:测试进程加载的仍然是旧的程序集。
3.2 根因定位过程
我做了三次排查实验:
第一次,怀疑是域名冲突或程序集缓存问题。我手动在编辑器里执行了一次Assets -> Open C# Project,再跑测试,通过。第二次,我删掉Library目录里跟脚本编译相关的文件夹,再跑,通过。第三次,我不做任何手动操作,加长两次调用之间的等待时间,再跑,依然失败。
这三组实验交叉对比后,问题基本锁定:批处理模式下一次以测试为目的的进程退出后,某些涉及脚本编译的状态被持久化到了 Library 目录。下次进程启动时,Unity 误以为程序集已经重新编译过,直接复用了旧的 IL 指令缓存。
其实这个问题的本质是 Unity 的增量编译(Incremental Compilation)优化导致的。每次CompilationPipeline.CompilePlayerScripts()触发编译,Unity 会做一次“哪些文件变了”的比对。按常理,如果 C# 文件内容改动过,MD5 发生变化,Unity 自然能感知到。但批处理模式下,连续两次进程启动,Unity 对ProjectVersion.txt、脚本文件的mtime和内容哈希的处理跟正常编辑器会话里不一样——部分缓存数据被直接写死到Library/Bee目录下一次启动时要校验的对象里了。
Library/Bee是 Unity 2020 之后引入的构建系统缓存目录,里面包含了大量编译中间产物和状态文件。如果你细看这个目录下的文件,会发现上一次编译的响应文件(response file)、以及各程序集的源文件列表。当新进程启动时,Bee 会把这些响应文件作为“编译输入”的基准。问题就出在:如果响应文件本身的修改时间比你的 C# 文件还要新,Bee 就会认为“没有新输入”,从而跳过重新编译。
这个坑在人工操作时几乎不会出现,因为人手动打开编辑器,Unity 编辑器本身会做一次完整的域重载和脚本刷新;而在批处理模式下,没有任何机制强制让 Bee 重新评估所有脚本。
3.3 解决方案:强制清理并重建 Bee 编译状态
我的修复方案并不复杂——在每次编译执行前,先清理与本项目相关的 Bee 编译状态缓存。代码写在CompileProject方法最前面:
// EditorToolchain.cs [MenuItem("Tools/Force Compile")] public static void CompileProject() { ForceCleanBeeCache(); CompilationPipeline.CompilePlayerScripts(); } private static void ForceCleanBeeCache() { string libraryBeePath = Path.Combine(Application.dataPath, "../Library/Bee"); if (Directory.Exists(libraryBeePath)) { Directory.Delete(libraryBeePath, true); Debug.Log("[Toolchain] Cleared Bee cache to force full script recompilation."); } }注意Application.dataPath指向的是Assets目录,项目根目录是它的上一级。删除Library/Bee时,Unity 进程本身正在运行,所以删除动作不会损坏正在使用的文件——Bee 会在下次需要时重建。实测删除后编译时间会从增量编译的几秒变成全量编译的十几秒,但换来的是确定性,值。
后来我又做了个更安全的版本:不直接删整个 Bee 目录,而是只删除它下面的BuildProgram相关子目录,但那玩意儿在不同 Unity 版本里结构会变,稳定性不如全删。
这里要提醒一点:删除 Library/Bee 缓存不会影响 Asset 导入状态,也不影响场景和资源序列化,所以不用太担心它跟 Library 目录的其它数据耦合。到目前为止我在 2020.3、2021.3、2022.3 三个版本上验证过,都正常。
4. 第二个大坑:Windows 平台日志编码乱码,Agent 直接“看不懂”测试结果
4.1 现象描述
跨过编译状态残留的问题后,工具链能稳定跑通“编译 → 测试 → 出报告”的流程了。但我很快发现,Agent 经常反馈说“无法理解测试输出”。
我一开始以为是 Agent 的提示词写得太模糊,后来亲自去翻日志文件,发现了真正的问题——Unity 的日志文件在中文 Windows 系统下以 GBK 编码输出,而 Python 脚本默认用 UTF-8 读取,解析出来的全是乱码。
具体现象是:日志里 Python 打印出来的测试摘要,凡是含英文的部分都正常,一旦碰上中文日志行,直接变成???或鈥?这类乱码。
这还不是最恶心的。最恶心的是,某些断言消息包含中文字符时,XML 测试报告虽然声明了encoding="UTF-8",但实际写入时如果强制用 UTF-8 编码,会造成部分中文字符损坏,导致 XML 解析器直接报“invalid byte sequence”。
4.2 Unity 日志编码问题详解
Unity 在 Windows 上输出日志时,如果没有显式指定编码,会调用系统默认 ANSI 代码页,而中文 Windows 的 ANSI 代码页是 GBK(936)。这跟 macOS 或 Linux 上默认 UTF-8 的情况完全不同,所以很多人开发时没遇到过,一上 CI Windows 机器就踩坑。
处理方案有两个方向:
方向一:改 Unity 启动参数,强制它使用 UTF-8。
Unity 支持环境变量UNITY_UTF8_LOGGING。在命令行启动 Unity 之前设置这个变量,日志文件就会以 UTF-8 编码输出:
export UNITY_UTF8_LOGGING=1Windows 下用 cmd 的话,就是set UNITY_UTF8_LOGGING=1放在调用之前。PowerShell 里则是$env:UNITY_UTF8_LOGGING=1。
方向二:让 Python 脚本做编码嗅探,自动识别日志文件的编码再读取。
考虑到我们团队有些同事的机器上可能没设环境变量,我最后两个方案都做了——脚本里先判断日志文件开头有没有 UTF-8 BOM,再尝试 UTF-8 解码,失败则降级到 GBK:
import codecs def read_log(path: str) -> str: with open(path, "rb") as f: raw = f.read() # 尝试 UTF-8,失败就退回 GBK try: return raw.decode("utf-8") except UnicodeDecodeError: return raw.decode("gbk", errors="replace")实际上,就算你设置了UNITY_UTF8_LOGGING,某些老版本的 Unity 也不会百分之百遵守这个变量,所以脚本层做编码降级是兜底方案,必须做。
4.3 让测试报告 XML 真正可解析
测试结果 XML 文件是 Unity Test Framework 生成的,路径由-testResults参数指定。这个文件对 Agent 至关重要,因为它包含了每条测试的状态、耗时、失败堆栈。
我遇到过一个问题:在某些失败的断言中,如果断言消息里带有尖括号或者引号,生成的 XML 会包含未转义的字符,导致xml.etree.ElementTree.fromstring()报错。
比如这段代码就可能炸掉:
Assert.AreEqual(expectedResult, actualResult, "Expected: <x>, Actual: <y>");如果expectedResult或actualResult是某种能打印出<和>的字符串,那个<x>就会被 XML 解析器当成标签起始符。
我的方案是:不直接解析原始 XML 报告,而是让日志先经过一个 sanitize 步骤。简单说,用 Python 的xml.sax.saxutils.escape()把<、>、&替换成实体,再丢给解析器。失败的消息行则直接从测试报告 XML 用正则提取,不依赖 XML 属性中包含特殊字符的情况。
5. Agent 侧的实现:从“能调工具”到“会判断结果”
5.1 工具定义与回调设计
工具层打通后,就要把 Agent 接进来。我用的是 OpenAI 的 Function Calling 风格——Agent 并不是自由地执行任意命令,而是在对话中它认为需要的时候,返回一个结构化 JSON,里面声明“我要调用工具 X,参数是 Y”,然后由中间件去实际执行,把结果回传给模型。
这个项目里我定义了两个最关键的工具:
{ "name": "unity_test_editmode", "description": "Run EditMode unit tests in the current Unity project. Return a summary of pass/fail counts and stack traces for failed tests.", "parameters": { "type": "object", "properties": { "testFilter": { "type": "string", "description": "Optional. Test filter expression, e.g. 'MyGame.PlayerLogicTests' to run only specific tests." } } } }{ "name": "unity_read_file", "description": "Read a file from the repository, used for inspecting source code, test output files, or logs.", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "Repository-relative file path, e.g. 'Assets/Scripts/PlayerController.cs'" } } } }第二个工具的加入是我踩了一个实际项目需求之后才加的:Agent 跑完测试,如果发现失败信息不足以判断原因,它需要自己去读源代码、读堆栈里提到的函数实现,才能给出准确的修复建议。所以一个允许 Agent 查看仓库文件内容的工具,是让 Agent 真正“有用”的关键。
5.2 测试结果的“摘要化”处理:不要直接把原始日志抛给模型
一开始我天真地把整个 Unity 日志文件内容全部作为工具返回值交给 Agent。结果有两个问题:
第一,Token 消耗巨大。一个大项目的 EditMode 日志文件动辄几百 KB,按 token 算,一次调用就能烧掉几万 token,成本高且没必要。
第二,上下文污染严重。Unity 日志里有大量与本次操作无关的 warning、资源加载信息,Agent 面对这些噪声,注意力会被分散,甚至给出错误结论。
正确的做法是在中间件里做一次“摘要化”。我只从 XML 测试报告和日志中提取以下字段,拼成一个紧凑的文本传给 Agent:
[Test Run Summary] Total: 128, Passed: 126, Failed: 2, Skipped: 0 Duration: 12.4s Failed Test 1: MyGame.PlayerLogicTests.Test_SpdBoostThreshold_ExactEdge Error: Assert.AreEqual failed. Expected: 10.5, Actual: 10.49999 Stack: at MyGame.PlayerLogic.CalculateBoost (GameplayCommons.cs:78) Failed Test 2: MyGame.InventoryTests.Test_StackLimitOverride Error: NullReferenceException: Object reference not set to an instance of an object Stack: at MyGame.Inventory.AddItem (Inventory.cs:142)这份摘要短小精悍,Agent 不需要费力在大量无关日志中找关键信息。实际体验下来,它判断结果的准确性明显提升。
5.3 Agent 的“技能”设计:不是每个任务都从头开始
再往后我引入了“技能(skill)”这个概念,也就是把一些常见任务编排成固定的多步执行流程。比如“改动一个 C# 文件后跑相关测试”这个流程,不再依赖 Agent 自行临场发挥,而是让它加载一个预设的 skill 定义:
- 技能输入:要修改的文件路径、修改描述
- 步骤 1:调用
unity_read_file读取原文件内容 - 步骤 2:Agent 根据用户描述生成 patch,由中间件执行写入
- 步骤 3:调用
unity_compile验证脚本编译是否通过 - 步骤 4:调用
unity_test_editmode,过滤条件为该文件所在命名空间 - 步骤 5:分析测试结果,如果失败,读取堆栈对应的代码文件,生成修复建议
引入 skill 的好处是:减少 Agent 的决策空间,跑得更稳。它不用每次判断“哦,我是不是应该先编译再测试?”,技能流程直接规定了这种顺序关系。
5.4 状态一致性判断:什么时候该让 Agent 直接重跑
Agent 判断一次测试结果后,可能会有三种动作:报告成功、报告失败并附上分析、或者主动决定重跑。我在中间件里加了约束——同一个测试任务,Agent 最多连续重跑三次。超过三次就必须停下来让人介入。
为什么需要这个限制?因为 Agent 在自我修复循环中,偶尔会出现“越修越坏”的情况。比如某个失败用例,Agent 修改代码后运行,发现原来两个失败变成一个失败,它可能认为自己修好了,但实际上另一个还没修;接着它再修改,结果又引入了一个新失败。如果没有重跑次数上限,Agent 会在这个循环里消耗大量时间。
实际观察中,连续两次重跑后仍然失败的,大概率是 Agent 没有理解根因,继续重跑只是烧钱。这时候我倾向于让 Agent 输出一份“我已尝试的修复与失败原因分析”,把判断交给人类。
6. 测试报告闭环:让 Agent 能定位到具体产品代码而不是只在测试层打转
6.1 从测试失败到产品代码的映射
一个经常被忽略但同时极其重要的点是:Agent 在分析测试失败时,不能只看测试文件本身,因为大部分失败信息只告诉你“断言没通过”,并不会直接告诉你“产品代码哪里写错了”。
举个例子:测试类MyGame.PlayerLogicTests.Test_AddForce_ChangesVelocity失败,堆栈显示PlayerPhysics.ApplyForce() at PlayerPhysics.cs:102。如果 Agent 只会读测试文件,它永远只能看到Assert.AreEqual那一段,看不到真正的 bug 在PlayerPhysics.cs里。
所以我在日志摘要里特意保留“调用栈中第一个产品代码文件”(即不在Tests/目录、不在-test程序集内的那个栈帧)的路径和行号。这个功能实现起来很简单——用正则解析at Xxx.Yyy (FileName.cs:Line)这种格式,然后过滤掉包含Test或Assembly-CSharp-Editor的行。
实践证明,这个细节极大提升了 Agent 的修复质量。以前它经常说出“我建议修改测试代码,让期望值和实际值一致”这种完全错误的建议——那是典型的“只看测试不看实现”的毛病。有了产品代码定位之后,Agent 能给出类似“我发现PlayerPhysics.cs:102处的velocity计算缺少Time.deltaTime乘积,这会导致加速度和时间无关”这样真正一针见血的分析。
6.2 测试报告归档与 Agent 的记忆扩展
另外一个让我觉得必要的模块是测试报告的归档管理。我把每次运行的测试报告 XML、日志、产物摘要统一按时间戳归档到项目外的目录:
test-reports/ 2025-03-15_14-20-00/ editmode-results.xml editmode-run.log compile-run.log summary.jsonAgent 可以通过unity_report_latest工具直接读取summary.json,也能通过unity_read_file加上相对路径去追溯历史。这个设计很像 MCP(Model Context Protocol)里的“资源”概念——给 Agent 提供自己的长期记忆和可查询的文件系统。
这里不得不提一下:让 Agent 直接访问文件系统是一个双刃剑。访问项目代码目录是必要的,因为它要读源码、看配置;但访问归档目录就没有必要了,反而可能让 Agent 去读一些旧报告干扰判断。所以我的文件系统工具做了白名单限制,只允许读Assets/、Packages/、ProjectSettings/和test-reports/这四个目录,其它目录一律拒绝。这个白名单约束后来也证明非常必要——有几次 Agent 因为读取了Library/目录下的临时文件,状态混乱了很长一段。
7. 更进一步的边界情况:PlayMode 测试、构建产物验证
7.1 PlayMode 测试要不要也给 Agent 用
我的工具链里包含unity_test_playmode,但实际使用频率远低于 EditMode。原因很明显:PlayMode 测试要真正进入 play 模式,启动游戏逻辑、加载场景,耗时更长,而且对环境要求更高——一些测试甚至需要真机或特定设备,这些在 CI 和 Agent 环境下都不好满足。
但这里有一个例外值得提:如果你的项目主要逻辑是纯 C# 无渲染层的,那么 PlayMode 测试在-nographics模式下完全能跑,而且能覆盖不少 EditMode 覆盖不到的时序逻辑。比如动画状态机、网络同步、协程调度这些只有在 play 模式下才有意义的内容。
我的建议是:一开始先把 EditMode 测试链路跑通,确认 Agent 能在 1 分钟内完成一轮“通知 → 编译 → 测试 → 报告”循环后,再引入 PlayMode。PlayMode 跑一轮至少几分钟,如果没有稳定的测试隔离和数据清理,Agent 很容易被“环境脏了导致的假失败”带偏。
7.2 构建产物验证:不只是“能 Build 出来”
编译和测试通过不代表构建产物一定可用。我做工具链时额外加了一个步骤:构建后检查关键产物文件是否存在、文件大小是否合理、时间戳是否为最近修改。
比如 Android 目标的libil2cpp.so,它在Assets/../Temp/StagingArea/下生成。如果这个文件为零字节,那构建铁定失败了;如果文件时间戳是旧的,说明构建根本没有触发重新链接。这些东西用脚本一查就知道,Agent 不需要去解析构建日志。
这里也顺带回答热搜词里的一个常见问题:“Unity GameAssembly.dll 的作用”——这是 IL2CPP 脚本后端在 Windows/Android 平台生成的核心运行时程序集,相当于你把 C# IL 转换到 C++ 之后再编译链接出的产物。GameAssembly.dll 是否正常生成、体积是否合理,是判断构建是否成功的一个重要依据。所以我在产物检查脚本里重点盯了它。
8. 实测效果与后续扩展方向
8.1 一轮完整循环的实测数据
拿我们项目做了一次标准验证:Agent 收到指令“给PlayerController类增加一个IsGrounded只读属性,然后跑测试”。它通过技能流程完成了以下任务:
- 读取
PlayerController.cs原文件,确认结构 - 生成 C# 代码 patch 并写入文件
- 调用
unity_compile,编译通过,耗时约 22 秒(含强制清理 Bee 缓存) - 调用
unity_test_editmode,过滤条件MyGame.PlayerLogicTests,执行 18 个测试,全部通过,耗时约 9 秒 - 返回结论“已新增属性,相关测试全部通过”,并在结果中附上了改动摘要
全程无人介入。如果放以前人工来操作,光是在编辑器和命令行之间切来切去,加上等待编译和测试的时间,差不多也要三到五分钟。Agent 自动跑完大概是 40 秒左右。这个差别在单次任务上不算夸张,但胜在稳定和可并行——同时开几个 Agent 进程处理不同模块的任务也不会互相干扰。
8.2 从工具链到项目协作平台的扩展
再往后我计划尝试把这套工具链接入我们内部的 Bot 平台:业务或策划同事在 IM 工具里直接发消息“帮我跑一下战斗模块的测试”,机器人就自动拉起 Agent,在隔离目录执行测试,把结果以摘要卡片的形式回传到群里。这个扩展从工具链上是完全现成的——Agent 已经是一个可编程、可远程调用的服务,剩下的只是接一层消息协议。
这也引出一个我个人很强烈的感受:AI Agent 落地到研发流程里,最难的其实不是模型理解能力,而是工具链的稳定性和确定性。模型理解错了可以引导,工具链如果不稳定,Agent 再聪明也白搭。所以这篇文章花大篇幅在讲那些日志编码、缓存残留、测试报告解析的问题,因为这些东西恰恰是决定一个 Agent 工具链能不能真正跑起来的关键。
8.3 给同类项目的一点建议
最后给大家几个避坑建议,都是这次实践中换来的经验:
- 如果你计划让 Agent 频繁驱动编译,尽量给 Unity 分配一台专用机器或容器,不要让其它构建任务跟它抢 Library 目录,否则“缓存冲突”类问题会频繁出现。
- 在 Agent 的工具层做清晰的语义化返回值,不要返回“命令执行成功”这种模糊信息。要返回“编译通过”、“编译失败:3 个错误,第一个在文件 X 行 Y”这种结构化结果。模型对模糊反馈的容错率比你想象的低。
- 给 Agent 限定重试次数和单次工具调用的超时时间。Unity 批处理模式偶尔会因为资源导入问题卡死,不设超时的话 Agent 会一直挂在那里等。
- 版本管理上建议把整个 Editor 工具链脚本放在单独的 asmdef 里,依赖关系好控制,也方便在后面逐步拆出更多“技能”时不影响主工程。
这套链路的本质思路其实不局限于 Unity——任何有命令行接口、有确定性输出的开发工具,理论上都能被 Agent 以类似方式驱动。编译、静态检查、测试、构建,这些“无聊但必要”的重复劳动,确实可以交给 Agent 去跑了。