Agent 真正进入开发环境之后,和普通的代码补全工具有一个很明显的区别:它开始参与整个任务,而不只是生成某一段代码。

一个稍微复杂一点的 Bug,往往需要先找到相关模块,顺着调用关系往下看,再判断问题究竟出在哪里,修改之后还要重新编译和测试。过去这些工作基本都由开发者自己完成,现在其中相当一部分可以交给 Agent。

但这并不意味着开发者只需要描述需求,然后等待结果。Agent 最适合解决的是大量搜索、整理和执行工作,真正涉及代码理解和结论判断的部分,仍然需要回到源码和实际运行结果上。

因此,Agent 比较适合放在一个完整的开发流程里,而不是单独当成“写代码工具”。


工作流

调查

一个任务进来之后,比较稳妥的做法通常不是直接让 Agent 修改代码,而是先让它完成一轮调查。

例如 KWin 里遇到一个窗口关闭动画异常的问题,第一步可以先让 Agent 找出相关的窗口生命周期、关闭路径、动画入口和现有测试,同时看看最近是否有修改过这部分逻辑的提交。

这类工作非常适合 Agent。它可以同时搜索很多文件,把原本需要花不少时间建立的代码关系先整理出来。

例如最终可能得到这样一条路径:

Window

close()

state change

animation

compositor

这个结果的价值在于缩短了进入代码的时间,但它还只是一个调查结果,而不是问题的最终解释。

理解

调查完成以后,真正重要的部分才开始。

假设 Agent 判断问题和 Window 状态提前变化有关,就需要继续顺着代码看下去:这个状态是谁修改的,什么时候修改,动画是在什么时机启动的,状态又是在什么时候被读取的,中间有没有异步调用或者线程切换。

大型 C++ 项目里,这些细节往往比表面上的调用关系更重要。很多问题看起来像是某个函数行为不对,最后真正的原因却可能是生命周期、状态同步或者线程上下文。

Agent 可以帮忙找到这些代码,但不能仅凭一段总结就算作已经理解了问题。

规划

复杂修改最好把调查和实现分开。

先让 Agent 把问题原因和准备修改的范围说明白,再进入修改阶段。这样做不是为了增加流程,而是为了在代码发生变化之前留出一个判断点。

尤其是涉及多个模块的时候,先确认 Agent 理解的调用路径和准备采用的方案,再开始编辑代码,通常更容易控制修改范围。

如果直接让 Agent “修掉这个 Bug”,它很可能在调查过程中不断扩大修改范围,顺便重构代码,或者根据自己的判断增加一些并非必要的改动。等修改完成以后,再回头判断哪些变化真正解决了问题,成本反而更高。

实现与验证

方案确定之后,Agent 执行修改就比较自然了。

可以把具体约束直接写出来,例如不改变现有生命周期、不引入新的全局状态、保持 X11 和 Wayland 的行为一致,并要求补充回归测试。

修改完成以后,不应该以“代码看起来没问题”作为结束,而应该继续让 Agent 编译、运行测试、检查 diff,并在必要的时候重新复现问题。

这一步很重要,因为 Agent 最擅长的是连续执行,而不是对自己刚才写出来的代码做绝对可靠的判断。


判断

不要把确认当成验证

Agent 比较容易出现的问题,并不是完全胡说八道,而是给出一个听起来非常合理、实际上证据不足的解释。

例如:

开发者:为什么这里会出问题?

Agent:因为窗口状态提前变化。

开发者:确定吗?

Agent:确定。

开发者:再确认一次。

Agent:还是这个原因。

看上去已经确认了很多次,但实际上没有任何新的信息。

第二次回答和第一次回答来自同一套上下文、同一套推理过程,本质上还是同一个结论。

真正有效的做法,是让问题变得更具体,并要求 Agent 回到代码中寻找可以被验证的证据。

例如:

如果问题真的是窗口状态提前变化,

那么 close() 返回以后,
AnimationController::start() 读取到的状态应该是什么?

请沿着实际调用路径证明这个结论。

如果这个假设不成立,
指出具体哪一处代码与它冲突。

这时候 Agent 不再只是重新描述自己的答案,而必须重新检查调用路径。

反问题

这实际上是使用 Agent 时非常重要的一种工作方式。

Agent 很擅长回答“这里发生了什么”,但真正有价值的问题往往来自“如果你的判断成立,那么别的地方应该出现什么”。

例如 Agent 判断 A → B 是问题的来源,就可以继续问:执行 B 时状态应该是什么,后面的 C 为什么没有受到影响,如果阻断 A 之后问题是否仍然存在,以及是否存在另一条不经过 A 的路径也能触发同样的问题。

这些问题并不一定需要全部交给 Agent。开发者自己已经对代码形成了一定理解以后,反而更容易提出这种问题,因为这些问题来自对代码结构的实际判断,而不是单纯让模型再解释一遍。

独立证据

最终结论最好来自不同来源的证据,而不是来自更多轮对话。

flowchart LR Agent[Agent 分析] Code[源码] Test[测试] Runtime[运行结果] Judgment[最终判断] Agent --> Judgment Code --> Judgment Test --> Judgment Runtime --> Judgment

比如 Agent 判断某个状态在错误的时间被修改,那么源码路径应该能够支持这个判断,Debugger 或日志应该能够看到相应的状态变化,测试应该能够稳定重现问题,修改以后现象也应该消失。

这些证据最终指向同一个结论时,可信度才真正建立起来。

所以,Agent 的验证能力并不意味着“让 Agent 自己再检查一次”,而是让整个工作流能够持续产生新的证据。


配置

项目规则

Agent 长期参与一个项目以后,最值得配置的往往不是一堆提示词,而是项目本身的工作规则。

例如构建命令是什么,测试应该怎么跑,哪些目录不能随便修改,Git 能做到什么程度,提交和推送有没有限制。这些东西应该集中放在项目级规则里,让每次进入项目的 Agent 都能看到。

一个简单的 AGENTS.md 可以类似这样:

# Project Rules

## Build

Use:

just build

## Test

Use:

just test

## Code

Follow existing Qt / KWin patterns.

## Git

Do not push.

Do not create commits unless explicitly requested.

## Workflow

For multi-file changes:

1. Read relevant code first.
2. Explain the implementation plan.
3. Wait for approval before editing.
4. Build and test after editing.

这里最重要的一点是控制长度。

项目规则应该放那些几乎每次工作都需要知道的约束,而不是把整个项目历史都塞进去。规则文件越来越长以后,真正重要的内容反而容易被淹没。

Skill

项目中还有大量只在特定任务里才会使用的知识,例如 KWin 的测试方法、Gerrit Review 流程、PMS 查询方式或者某个平台的特殊构建步骤。

这些内容更适合做成 Skill。

这样项目规则只负责描述“这个项目平时应该怎么工作”,Skill 则负责“遇到这类任务以后应该怎么做”。

长期来看,这种拆分比不断向一个 Prompt 里面添加规则更容易维护。

Tool

Tool 解决的是另一件事:Agent 到底能不能真正把任务执行下去。

Git、Shell、LSP、Debugger、测试工具,甚至项目内部的脚本,都可以成为 Agent 的工具。

这里有一个很实用的原则:能确定执行的事情,就尽量交给确定性的工具。

比如检查 Git 状态、运行固定测试、生成代码、收集特定日志,这些都不需要让模型每次重新决定执行方式。已经有脚本或者专用 Tool 的情况下,Agent 只需要决定“现在要做什么”,具体怎么执行由工具完成。

这样做通常比让模型自己拼命令更稳定,也更容易控制。


工具

OpenCode

OpenCode 比较适合需要完整 Agent 工作流的场景,Agent、权限、Subagent 和 Session 这些能力都比较明确。

Pi

Pi 的方向更偏轻量,核心保持简单,再通过 Extension、Skill、Tool 等方式扩展。

这种方式比较适合已经有一套脚本和开发工具体系的项目。很多原本依赖模型判断的固定流程,都可以逐渐收敛成 Extension 或脚本,让 Agent 负责调用,而不是每次从零决定怎么做。

Oh My Pi

Oh My Pi 在 Pi 的基础上进一步强化开发场景,加入了 LSP、DAP、Subagent 和 Memory 等能力。

其中真正值得关注的并不是功能列表本身,而是这些能力能不能进入实际工作流。

例如 LSP 可以减少纯文本搜索,DAP 可以让 Agent 进入 Debug 阶段,Subagent 可以把独立调查隔离出去,而 Memory 则负责把过去的项目经验带到新的 Session。


Context

上下文不是越多越好

Agent 的上下文窗口越来越大以后,很容易产生一个误区:既然能放很多内容,就尽量让 Agent 多读一点。

实际使用中经常会发现,Context 太满以后,效果并不会线性变好。大量无关源码、工具输出和历史尝试堆在一起,真正重要的信息反而更容易被淹没。

所以问题从来不是“Context 够不够大”,而是“当前任务真正需要哪些 Context”。

大型 C++ 项目里尤其如此。通常应该先搜索符号、找到定义和引用,再逐步收敛到关键文件,而不是一开始就把整个模块全部读进来。

Session 不宜无限增长

另一个常见问题是一个 Session 持续工作太久。

一个 Bug 做到一半,顺手又解决了另一个问题,后来又发现一个重构机会。任务越做越大,Context 越来越长,最终 Agent 已经很难区分哪些内容是当前任务,哪些只是之前尝试留下的历史信息。

所以复杂任务拆成调查、规划、实现和 Review 几个阶段,通常比让一个 Session 一直干下去更容易控制。

Subagent 的价值也主要体现在这里:可以把一些相对独立的调查放进单独上下文,不影响主任务。


Memory

长期经验

Session 解决的是当前任务,而 Memory 解决的是跨任务的经验积累。

例如第一次处理某个平台问题时,确认了某个测试必须提前启动一个服务;或者某个模块存在一个不能跨线程访问的约束;又或者某个历史 Bug 最终确认是对象生命周期的问题。

这些信息在当前任务结束以后仍然有价值。

相反,今天执行过哪些临时命令、某次测试失败时的中间日志,这些内容通常不值得全部保存。

因此 Memory 更应该记录那些未来还可能影响判断的信息。

经验复用

Memory 的价值也不是简单地“记住过去”。

例如某个测试第一次失败以后,已经确认这是一个平台环境问题。几天以后再次出现同样的错误,新 Session 如果能够先召回这条经验,就可以直接检查相关环境,而不需要把整个调查重新做一遍。

flowchart LR Past[过去的经验] --> Memory[长期 Memory] Memory --> NewTask[新的任务] NewTask --> Agent[Agent 决策]

这才是长期 Memory 真正产生价值的地方:它开始影响下一次任务的调查路径。

OpenViking

当项目资料越来越多以后,Memory 的难点就不再只是“保存”,而是“找到当前真正相关的内容”。

代码、文档、需求、Bug、测试记录和历史经验都可以成为上下文,但一个具体任务通常只需要其中很小的一部分。

OpenViking 更适合处理这种资源和上下文组织问题。它并不是单纯增加一个存储位置,而是让 Agent 能够围绕当前任务去找到更合适的内容。

Hindsight

Hindsight 更偏向长期 Memory 本身,retainrecallreflect 分别对应经验保存、相关记忆召回以及对已有记忆进行综合。

这类机制适合长期运行的 Agent,因为项目经验不是一次性的。一个问题可能在几周以后再次出现,而当时积累的判断过程、限制条件和最终结论,都会成为下一次任务的参考。

但 Memory 始终只是历史经验,不应该被当成绝对事实。

项目代码发生变化以后,过去记录的经验可能已经过期。当前源码、当前测试结果和真实运行行为永远优先于历史 Memory。


踩坑

Prompt 过长

Agent 刚开始使用的时候,最容易做的一件事情就是不断往规则文件里加内容。

每遇到一次问题,就加一条规则;每踩一个坑,再加一条说明。最后整个 Prompt 变得非常长。

这种方式短期看起来有效,时间长了以后反而难维护。

项目规则应该保留高频约束,专项知识放到 Skill,长期经验放到 Memory。这样不同信息才有各自稳定的位置。

让 LLM 决定所有事情

另一个容易出现的问题,是把所有操作都交给模型。

一个本来可以用固定脚本完成的事情,却让 Agent 每次重新判断应该执行什么命令;一个固定测试流程,却让模型自己决定测试顺序。

这种工作方式不仅浪费 Context,也容易让 Agent 在一些边缘情况下偏离既定流程。

能够程序化的部分尽量程序化,把模型留给真正需要判断的事情,通常效果更稳定。

一个 Session 做太多事情

Session 越长,并不意味着 Agent 越聪明。

当上下文里同时混进多个问题、很多失败尝试和大量无关工具输出以后,Agent 很容易开始丢失任务边界。

复杂任务拆分成几个独立阶段,往往比不断追加上下文更有效。

把 Agent 的自我检查当成最终 Review

Agent 可以 Review 自己刚才的修改,但这只能算第一轮检查。

真正重要的还是编译、测试、运行、日志、Debugger 和 Diff。

“这次修改应该没有问题”只是一个判断,而不是证据。


一次完整的任务

把这些东西组合起来,一个实际任务可以保持在比较简单的路径上:

flowchart LR Task[任务] --> Explore[调查] Explore --> Understand[理解] Understand --> Plan[规划] Plan --> Implement[实现] Implement --> Verify[验证] Verify --> Memory[经验沉淀]

比如一个 KWin Bug 进入以后,先让 Agent 找相关代码和测试;调查结果出来以后,开发者自己跟踪关键调用路径,确认状态、线程和生命周期,再决定具体怎么修改。

方案确定以后,Agent 负责实现和测试。修改过程中产生的证据再回到问题本身,确认之前的判断是否真的成立。

如果最终确认某个平台存在一个长期有效的特殊约束,再把这条经验沉淀到 Memory。下一次遇到类似问题,Agent 就可以从已有经验开始,而不是再次从零搜索。

整个过程看起来仍然是传统的软件开发流程,只是其中大量机械性的工作已经被 Agent 接管了。


结语

Agent 真正带来的变化,并不是单纯把代码写得更快,而是把搜索、整理、执行、测试这些原本分散在开发过程中的工作串起来。

比较稳定的方式,是让 Agent 负责大量可以自动化的工作,同时把代码理解、设计和最终判断保留在开发者手里。

项目规则负责约束常规工作,Skill 负责专项流程,Tool 负责确定性的执行,Context 负责控制当前任务需要看到的信息,Memory 则把已经验证过的项目经验带到未来。

当这些东西逐渐组合起来以后,Agent 才会从一个偶尔帮忙写代码的工具,变成真正能够长期参与项目开发的协作者。