用PI控制器思路调校AI编程助手:从上下文爆炸到稳定输出 1. 给编程助手取名Pi从对话打字员到自治开发小组如果你和我一样每天要在编辑器里和AI编码助手来回拉扯十几个回合你一定体会过那种感觉它能写但忘得快。项目刚开始时很爽到第三周你开始怀疑它是不是失忆了——上一轮刚交代过的接口命名规范下一轮就当成闲聊忘干净了。我搭这个叫Pi的编程助手起因就是这件事。为什么叫Pi一半是因为圆周率无限不循环象征一个永远在逼近目标、但每次都需要修正的响应过程。另一半是我的个人解释Personal Intelligence个人维护的智能系统。这两种解释恰好对应了Pi的两个设计原则反馈修正、个体自治。Pi不是那种多聊几轮就能记得更多的助手。我的核心思路是与其把单个Agent调成记忆力很好的打字员不如把它变成一支分工明确的开发小组。这个小组里有负责动手的执行者、负责挑刺的审查者、专门写测试的测试者每个人只维护自己那一小块上下文通过统一的调度接口协作。这个思路实施下来上下文压力小了很多生成的代码也明显更有章法。如果你本来搜的是树莓派Raspberry Pi先说明一下这里说的不是硬件板卡。去年我确实在树莓派Pico 2040上接过一块0.96寸OLED那是另一个有趣的话题而且控制领域的PI参数在那里也一样重要。做硬件的人看到SI/PI会想到信号完整性和电源完整性搞控制的人看到PI会想到比例积分控制器软件圈的人看到Pi会想到树莓派和圆周率。我今天说的这个Pi正好站在它们的交叉点上——用控制理论的思想给AI编程助手做反馈设计。1.1 单人开发的核心问题不是模型不行是单人不行现在的编码模型已经很能打了真正让开发效率上不去的是上下文管理。一个Agent会话里塞下几百行代码、十几个需求变更、几轮修正意见之后它开始分不清哪些是当前要改的哪些是当初说好的全局规范。于是你看到的现象就是改一处接口它把半个项目的依赖都牵连上改一个按钮文案它把样式文件重构了一遍。这个问题不能靠把prompt写得更长解决因为上下文越长越容易让Agent把注意力均匀摊到所有信息上关键约束反而被稀释。我的做法是反过来的把大上下文拆成小上下文每个子代理只掌握完成自己那部分任务需要的最小信息集。1.2 Pi的三个基础组件Skill、Subagent和统一入口Pi的架构可以用三句话概括。Skill是操作规范包把常见的开发流程固化成可导入的步骤文件Subagent是分工角色每个角色有自己的职责、输入、输出和决策权重统一入口是调度器负责接收你的自然语言指令拆解后分派给合适的子代理再把结果汇总回来。这三个组件不是独立存在的。Skill决定了Subagent做事的方法Subagent产生的输出又会被调度器当作下次分派的依据。用控制理论的话说调度器承担的是设定点的角色——你告诉它目标它不断比较目标与当前输出的差距然后调整各子代理的工作。这套结构搭好之后你不需要对每个任务重新写一遍完整prompt只需导入对应的Skill剩下的是调度的问题。1.3 为什么拆成子代理能缓解上下文爆炸一个很直观的原因统一Agent需要同时知道接口定义、数据库表结构、前端组件规范、测试框架用法它一次读取的信息量非常大。拆成执行者、审查者、测试者之后执行者只需要知道接口和数据库测试者只需要知道测试框架和接口的输入输出样例审查者则需要知道全局规范。各取所需单个模型处理一个任务时拥堵的其实是注意力带宽而不是字面意义上的存储上限。这也是为什么我会用控制论的术语来理解Pi的参数而不是把它当纯提示词工程。如果一个子代理响应太激进问题不完全出在提示词写得不够好而是增益设置不合理。这就引出下一节要说的内容控制工程师天天在调的Kp、Ki和带宽其实也可以用来配置Agent。2. 用PI控制器的思路配置PiKp、Ki和带宽的Agent化先给你打个预防针下面说的不是严格的控制数学推导而是一个经过工程验证的迁移类比。不要小看这个类比控制理论的核心逻辑——误差、反馈、增益、带宽——在Agent系统里都能找到实实在在的对应物。2.1 控制回路三要素在Agent里的对应关系一个经典的单回路控制系统中有被控对象、控制器、反馈传感器。搬到Agent系统里被控对象是正在生长的代码库控制器是Agent的推理循环反馈传感器是编译器输出、测试结果、你的审查意见。误差信号则是目标结果与当前产出之间的差距。控制器做的事情和人做项目管理一样根据差距的大小决定下一步动作。差距大动作就要大差距小做微调。这正是Kp比例增益干的事。而Kp如果设得过大系统就会超调——对应到代码上就是你让它加个日志它顺手把整个模块的错误处理机制重写了。2.2 比例增益Kp指令遵循强度不是越高越好Kp在Agent配置里的第一个体现是指令的强制程度。我见过不少人的prompt里用了一堆必须、一定、无论如何都要等于把比例增益拉满。结果呢Agent确实更听话了但它会把每条约束都当成最高优先级导致任务拥挤。你让它修一个空指针异常注意保持现有风格它会非常用力地理解保持现有风格最后为了风格统一把引号都改了一遍。调Kp的正确姿势是给指令分级。在我的Pi里我用词汇来区分顶级约束用唯一必须遵守的是普通约束用如果没有特殊情况可以不用管建议用可做可不做。这比在一条指令里把所有要求都写成最高级可靠得多因为Agent对文本强度的响应是非线性的——所有事情都是最高优先级等于没有优先级。这个Kp正好是控制工程师天天打交道、也是大家常搜的那个k pi。2.3 积分项Ki项目历史记忆的累积与饱和Ki的对应物是Agent对项目历史的记忆强度。积分项的本意是消除稳态误差即使当前误差很小只要历史上有持续偏差控制器也会累积修正量。Agent里的历史偏差就是那些你反复纠正、但它一直不稳的行为比如缩进用两个空格还是四个空格测试文件路径放在哪。但积分项有个经典副作用叫积分饱和——误差累积到一定程度修正量直接顶到执行机构的上限。对应到Agent里就是你给它的上下文里堆了太多历史修正记录它每一次响应都会把这些记录重新翻一遍结果动作变得又慢又僵。我实测下来的解法是定期给上下文做抗饱和处理把过去五轮修正意见压缩成一条结构化约束放进项目级配置而不是继续堆在对话历史里。2.4 带宽fb上下文容积与工具调用频率的匹配带宽在控制系统里指系统能有效响应的信号频率范围。放到Agent系统里最接近的是上下文容积与任务复杂度的匹配度。上下文窗口大意味着Agent能看全局代价是高带宽会放大噪声——一个不相关的历史细节、一段被误读的老代码都可能被它拿过来当决策依据。上下文窗口小又会导致它只盯着眼前任务看不到整体风格。所以带宽不是越大越好而是要和任务匹配。Pi的做法是动态调整简单任务改个文案、加个常量给子代理的上下文窗口缩到最小重构任务则把仓库结构、受影响模块、历史规范一次性喂进去。这个调带宽的过程跟锁相环里根据锁频需求调节控制带宽fb的逻辑是一样的——带宽太大高频噪声进来带宽太小动态响应跟不上。如果你做过电力电子控制一定也被MMC环流抑制器的PI参数折磨过Kp大了发散Ki大了饱和最后老老实实按Ziegler-Nichols法的思路先调比例、再加积分、最后看带宽。Agent调参也一样。2.5 一张映射表把控制语言翻译成Agent配置语言控制术语Agent对应物参数偏大的表现参数偏小的表现参考整定手段Kp 比例增益指令遵循强度/单步动作幅度超调无关改动多风格过度统一响应迟钝说好几遍才动手给指令分级避免所有约束都拉满Ki 积分增益项目历史记忆权重积分饱和上下文膨胀、响应僵化稳态误差老问题反复出现定期压缩历史为结构化约束fb 带宽上下文容积与工具调用频率噪声放大无关信息干扰决策响应跟不上只见局部不见整体按任务复杂度动态调整上下文窗口设定点任务目标描述目标含糊各子代理各自为政目标过于绝对耦合度下降把目标拆成可验收的交付物标准这张表是我实际调校Pi时贴在项目文档第一页的东西比任何提示词模板大全都管用。理解了对应关系后面每一次调整就都有依据而不是瞎试。3. Pi的搭建过程Skill导入、Subagent分工与桌面端管理理论说得再多不如亲手把架构搭出来。这一节我按自己当时的实际操作顺序来写包含完整的配置文件示例和命令。3.1 先做减法把工作流拆成最小Skill集合搭Pi的第一件事不是去网上找一堆编译模型的prompt而是自己做减法。我从日常开发流程里挑出了四个出现频率最高的场景方案设计、编码实现、代码审查、测试补齐。每个场景对应一个Skill文件。Skill的本质不是新功能而是行为契约——它约束子代理在什么前提下、按什么顺序、以什么格式做事。下面是一个简化版的plan skill文件YAML格式name: plan-before-code version: 1.0.0 description: 先输出实现方案获得确认后再动代码 trigger_verbs: [实现, 修复, 重构, 增加] steps: - step: read_repo_structure param: top_level output: [affected_files] - step: propose_plan output: [changes, risks, verification_steps] - step: wait_for_confirmation ok: proceed cancel: return_to_propose constraints: - 不允许在方案确认前创建或修改文件 - 方案必须明确列出不改动的范围避免无关牵连我故意加了不改动范围这一条就是吸取了Kp过大的教训有些改动它不是能力问题而是增益问题。然后在Pi的命令行里导入skillpi skill import ./skills/plan-before-code.yaml pi skill listWeb端也支持直接粘贴YAML导入方便你从一个项目里导出、再到另一个项目导入。我后来把常用的几个skill放到一个gist仓库里做版本管理换机器时一条命令拉下来不用重新手敲。3.2 Subagent角色设计执行者、审查者、测试者Skill定义怎么做Subagent定义谁来做。我最初只建了一个coder结果它自己写自己审错误率极高。后来参考现实团队的分工拆成了三个角色每个角色都有自己的职责边界、输入来源和输出格式。coder的配置{ role: coder, mission: 根据确认后的方案实现代码改动, input: [plan_output, affected_files, repo_style_guide], output: diff格式的改动结果附带必要说明, authority: 1.0, guardrail: 不修改plan中标注为不改动的范围 }reviewer的配置{ role: reviewer, mission: 审查diff找出逻辑漏洞、风格偏离、冗余改动, input: [git_diff, project_style_guide, task_title], output: 结构化审查意见每条标注严重级别B(必须改)/C(建议改)/D(可忽略), authority: 0.7, guardrail: 不直接改代码只输出意见 }tester的配置{ role: tester, mission: 为本次改动补充或更新测试用例, input: [git_diff, test_framework_config], output: 测试代码diff以及一条最小验证命令, authority: 0.6, guardrail: 不修改业务代码只改测试目录 }为什么reviewer的authority要比tester高因为长期实践下来代码风格和逻辑正确性比测试覆盖率更容易成为项目瓶颈。给reviewer更大的增益等于把质量反馈回路放大。这里的authority值说白了就是PI控制里那个Kp的权重分配只不过它作用在Agent协作的决策层。3.3 通过Web端导入Skill用桌面端做日常总控先说一句光有命令行跟只有命令行都不够。如果Pi只是建在终端里你每次要用它就必须打开那个终端任务一多就容易乱。所以我把Pi的入口分成了两层Web端负责skill导入、配置管理和历史任务查询桌面端负责日常总控、任务派发和结果预览。常规做法是找一个现成的桌面管理壳比如社区里的Oh My Pi这类方案把多个Pi项目的配置聚到同一个图形入口下操作统一且可追溯你也可以自己写一个简单的Electron壳核心目标是一样的。Web端的导入流程很简单打开任务面板拖入YAML文件系统校验格式通过后加入待激活列表重启调度器后生效。几个测试性改动调通后一定要把skill文件纳入git仓库版本回滚和多人协作都方便。桌面端放的是任务总览你可以看到当前正在执行的子代理、它们各自的输入输出、以及最后一次反馈的状态相当于一个Agent团队的驾驶舱。3.4 接进命令行和编辑器的最终形态最终我习惯的用法是这样的编辑器里写好任务描述用命令行唤起Pi指定任务类型和优先级pi run --skill plan-before-code --role coder \ --message 修复登录接口的空指针异常不动认证流程调度器解析任务后先把任务交给plan skill方案确认后交给codercoder完成后自动调起reviewerreviewer给意见后如果是B级意见调度器再把diff传回coder修改。这个循环跟PI控制器没本质区别——目标值就是修好空指针且不动认证流程反馈量就是reviewer的意见和测试结果输出就是不断收敛的diff。4. 实测调参用RSS解析器任务看Pi从振荡到稳定光看配置不看实测总感觉隔了一层。我选一个大家都见过的任务来演示用Python写一个RSS解析器。任务描述很简单输入一个RSS订阅源地址输出文章列表包括标题、链接、发布时间。为了让调参过程更有挑战我还额外要求保持代码风格与项目一致、单测覆盖核心逻辑。4.1 第一次手术Kp过高行动太快导致质量波动第一版配置我用了大部分人都默认的做法把约束全部写进一条指令期望它一次性搞清所有事。结果Pi非常积极几秒内就给出了一大段实现代码。但问题马上就来了它为了保持风格一致把我项目里一个自研的HTTP客户端也顺手换成了requests为了单测覆盖连setup.py的入口点都改了。这属于典型的超调——Kp太高步子迈得太大。解决的办法不是重新写prompt而是给调度器增加一条动作前检查规则在coder动工前先把它的产出预案发给reviewer做一次改动范围评估只有改动范围合格的才能进入编码。相当于给控制器串了一个限幅环节把单步输出的最大变化量限制住。这是控制工程里最基础的抗超调手段搬到Agent流程里同样好用。4.2 第二次手术Ki积分饱和上下文越拖越重第二轮方案确认没问题coder开始出代码了。但跑着跑着我又发现一个更隐蔽的问题对话轮次一多coder的输入里堆积了大量修正历史。比如第一次reviewer说不要用requests第二次说函数命名别缩写第三次说文件放错目录了——到第五次修改时coder需要先处理这四轮历史再去做新改动。一个简单的解析器最后生成的代码膨胀到了原来的三倍全是历史修正叠加的痕迹。这就是积分饱和的典型症状。我的处理方式是在调度器里加了一个记忆压缩器每轮调整结束后把该轮的修正意见摘要成一条当前约束替换掉原来的长历史。这样coder面对的上下文始终是精简版规范而不是全量对话历史。效果立竿见影rss_parser_title这种被历史修正逼出来的缩写命名也回到了正常可读的rss_entry_title。4.3 第三次手术带宽失配三个Subagent互相踢皮球第三轮上下文精简了但新的问题浮出来reviewer和tester频繁互相等待。coder改一次reviewer看一遍给出几个C级意见和两个D级意见tester又看一遍补充两个建议。调度器把意见全部发回给codercoder一改又触发新一轮review。整个循环来回六次才稳定耗时比预期多了四倍。我去看日志发现大部分时间都耗在低价值反馈的传递上。低价值反馈就相当于控制系统里的高频噪声。我的调整是在调度器层面把C级和D级意见聚合累积到一定数量才触发下一轮修改B级意见则立即返回。同时把tester的authority降到了0.4因为对解析器这种纯逻辑模块来说测试框架用对即可测试用例的具体写法不用它说了算。这就是带宽匹配不是所有反馈都要以同样的优先级实时传导该滤波的滤波该放大的放大。4.4 调参顺序的通用套路先降比例再减积分最后调带宽经过三轮调整RSS解析器的最终配置是plan skill严格生效、记忆压缩器默认开启、审查意见分级联动。整个过程其实很容易总结成一套可复用的顺序。先降比例因为比例增益最容易产生肉眼可见的破坏再减积分解决长期累积的僵化问题最后调带宽处理那些不是某个单一参数太大而是多个反馈通道节奏不合拍的情况。我把这三步写成了一个checklist每次接到新的Agent项目都按这个顺序过一遍。我自己的体会是这套和Ziegler-Nichols的整定思路高度一致先把系统逼到临界状态找到规律性振荡再按经验公式退到安全值。Agent不会给你递幅频曲线但它会通过行为模式把它自己的临界比例增益暴露出来——那种改一个按钮它重构了样式表的时刻就是最好的指示器。5. Pi的适用边界和留给下一阶段的事把Pi调顺之后我确实一度想让它接越来越多的活。但用了几周我对它的边界也有了更清醒的认识。这一节说说哪些任务适合Pi哪些不适合以及我下一步打算做什么。5.1 什么任务适合交给Pi什么任务不适合适合的探索性编码、脚本工具、文档整理、原型验证以及一切结果可以被测试和review兜底的任务。这类任务的特点是容错率高即使子代理某次反馈不好也不至于造成不可逆的损失。不适合的安全敏感代码、涉及用户敏感数据处理的逻辑、需要强确定性审计的规则系统以及任何一旦出错就是事故的场景。并不是说Pi能力不够而是它的反馈回路——包括reviewer的意见和我的审查——本质上是在尽量逼近目标而不是精确保证。安全系统需要的是形式化验证、审计链路这不是调几个参数能补上的。重要代码我依然保留人工走查的步骤。提示Skill文件格式尽量保持版本化配置变更要走代码审查流程否则你的Agent行为变更会成为一个说不清的黑盒。每次调整都留记录出问题才能回滚。5.2 Skill库的版本化与多项目复用Skill拆开之后复用价值比我预期高得多。同一个plan-before-code我拿到另一个项目就是改一下trigger_verbs和style_guide路径其余不用动。更进一步我开始把跨项目的通用规范抽成公共skill比如commit信息格式、错误处理约定、日志规范这类内容已经接近团队开发规范了。Skill库放进git仓库后可以像管理依赖一样管理行为契约的版本某个项目锁死某个skill版本等验证稳定后再统一升级。5.3 把Pi接进CI/CD之后的想象空间下一步我打算做的是给Pi接上自动化流水线。最简单的接法是在CI里跑一个reviewer子代理每次PR生成后自动调用reviewer技能审查diff输出B/C/D分级意见作为人工评审的辅助信息。再往后还可以把测试生成和文档更新都做成CI阶段的任务。但注意我暂时不会让Pi自动合代码因为自动合并就是去掉最后一个反馈环节等于把回路断开。控制工程师都知道反馈回路一旦断开系统就不可控了。5.4 我的体会调Agent和调PI本质都是每次只动一个变量最后说说我最大的体会。这次给Pi调参的过程让我重新理解了为什么控制工程师在整定PI参数时强调一次只动一个变量。因为多变量联动的时候你永远不知道当前的表现到底是哪个参数引起的。Agent配置比PI回路复杂得多变量也更多但这条原则反而更关键。我有一次调完上下文窗口后又顺手改了reviewer的authority结果系统从稳定变成频繁振荡花了一上午才发现是后一个参数的问题。所以我的习惯是每次调整只动一个配置记录下调整前后的行为差异隔一段时间再回头看。这听起来像老生常谈实际操作中却最容易违反。Pi从金鱼记忆到稳定输出不是靠某个神级prompt靠的就是这种一次一变量、反馈驱动修正的笨办法。这大概也是它叫Pi的最贴切原因——无限逼近持续修正这正是我最想从Agent系统里拿到的东西。