☰
AI Agent稳定性工程:用PID与ADRC控制理论解决智能体可靠性困境
2026/9/26 8:17:03 网站建设 项目流程

1. 智能体可靠性困境的本质:为什么“聪明”不等于“稳”

做AI Agent开发的人大概都有过这种体验:demo阶段惊艳四座,一旦放到真实环境里跑上几天,各种诡异问题就开始冒出来。工具调用偶尔失败、多轮对话中上下文漂移、任务执行到一半突然“忘记”了前面的约束条件、同一个输入两次运行结果差异巨大。这些问题有个共同特征——它们不是“能力不足”,而是“稳定性不够”。

我把它叫做**“脆弱的聪明”**:Agent在大多数时候表现得很智能,但缺乏对扰动的抵抗能力,一旦外部条件偏离训练或调试时的假设,整个系统就像多米诺骨牌一样崩塌。这个问题的根源,其实不在模型本身,而在于我们构建Agent时默认了一套“开环思维”——规划好步骤,然后一步步执行,假设每一步都会按预期发生。

但真实世界从来不是开环的。

1.1 从控制论视角重新理解Agent

控制论里有个核心概念叫反馈闭环。一个系统如果只有前向通路而没有反馈通路,那它对外部扰动的抵抗能力几乎为零。传统的AI Agent架构——无论是ReAct、Plan-and-Execute还是更复杂的多Agent协作——本质上都是“规划-执行”的开环结构。LLM负责规划,工具负责执行,中间缺少一个实时的、连续的偏差修正机制。

这就引出了一个有意思的类比:Agent的可靠性问题,和工业控制里电机调速的稳定性问题,在数学结构上高度同构。电机面临负载扰动、摩擦变化、电压波动,Agent面临API延迟、工具返回异常、上下文噪声、模型输出随机性。两者都需要一个“抗扰动”的控制器来维持系统稳定。

工业界解决这类问题已经有非常成熟的理论体系——PID控制和它的进阶版本ADRC(自抗扰控制)。这套东西在电机控制、无人机飞控、化工过程控制里跑了几十年,稳定性和鲁棒性经过了无数验证。把它迁移到AI Agent的可靠性工程里,是我最近半年一直在折腾的方向,实测下来确实能解决不少“玄学问题”。

1.2 谁适合参考这套思路

这篇文章适合两类人:一是正在做AI Agent开发、被稳定性问题折磨的工程师;二是对控制论有基础、想看看经典控制理论怎么和LLM系统结合的跨界玩家。不需要你精通控制论,但需要对Agent的基本架构(工具调用、规划、记忆)有实操经验。我会尽量用生活化的类比把控制论的概念讲清楚,同时给出可以直接抄的代码和参数。

2. PID控制:最容易被低估的Agent稳定性工具

先说PID。很多人一听PID就觉得“太老了”“太简单了”,但恰恰是这种简单,让它在Agent场景里出奇地好用。PID的核心思想就一句话:根据当前误差、历史误差累积、误差变化率,计算出一个修正量。

2.1 PID的三个分量在Agent里分别对应什么

比例项(P)对应的是“当前偏差有多大,就修正多少”。在Agent里,这可以映射为:当前工具返回结果与预期目标的差距,直接决定下一步动作的调整幅度。比如Agent在调用搜索工具时,如果返回结果的相关性评分低于阈值,P项就会产生一个“加大搜索范围”或“换关键词”的修正信号。

积分项(I)对应的是“历史偏差的累积”。这个在Agent里特别有用——如果某个工具连续多次返回不理想的结果,I项会逐渐累积,最终触发一个“切换工具”或“升级策略”的动作。没有I项的话,Agent可能会在同一个工具上反复试错,陷入局部循环。

微分项(D)对应的是“偏差变化的速度”。如果误差正在快速缩小,D项会抑制过度修正,防止Agent“矫枉过正”。比如在多轮对话中,如果用户意图已经逐渐清晰,D项会防止Agent继续追问过多澄清问题。

class PIDController: def __init__(self, kp, ki, kd, setpoint=0.0): self.kp = kp self.ki = ki self.kd = kd self.setpoint = setpoint self.integral = 0.0 self.prev_error = 0.0 self.dt = 1.0 # 每次Agent循环的时间步长 def compute(self, measured_value): error = self.setpoint - measured_value self.integral += error * self.dt derivative = (error - self.prev_error) / self.dt output = (self.kp * error + self.ki * self.integral + self.kd * derivative) self.prev_error = error return output

这段代码可以直接嵌入Agent的决策循环里。measured_value可以是工具返回的置信度、任务完成度评分、或者任何你定义的“系统状态指标”。output就是下一步动作的修正量。

2.2 参数整定的实操经验

PID最难的地方是调参。我在Agent场景里试过几种整定方法,分享几个实测有效的经验:

先调P,再调I,最后调D。这是经典顺序,在Agent里同样适用。P太大,Agent会频繁切换策略,表现为“行为抖动”;P太小,Agent反应迟钝,任务完成率下降。我的经验值是P从0.3开始试,每次增加0.1,观察Agent在标准测试集上的任务完成率和行为方差。

I项要加限幅。Agent场景里,积分饱和是个大问题。如果某个工具连续失败,I项会无限累积,导致Agent做出极端决策(比如直接放弃任务)。加一个积分限幅,比如self.integral = max(min(self.integral, 10.0), -10.0),能有效防止这种情况。

D项对噪声敏感,要慎用。LLM的输出本身就有随机性,D项如果太大,会把这种随机性放大成剧烈的策略震荡。我一般把D设得很小,或者干脆先用PD控制跑一段时间,等系统稳定了再加D。

注意:PID的参数没有“万能值”,必须根据你的Agent任务类型来调。任务型Agent(比如订机票、查天气)和对话型Agent(比如客服、陪伴)的最优参数差异很大。建议先在一个固定的测试集上跑50-100个case,记录任务完成率和行为方差,再开始调参。

2.3 一个真实的调参案例

我之前做一个电商客服Agent,主要任务是处理退换货请求。初始版本用纯LLM规划,任务完成率大概72%,但方差很大——有时候连续10个请求都成功,有时候连续5个都失败。后来加了一个简单的P控制器,根据用户情绪评分(用一个小模型实时打分)来调整Agent的回复策略。P参数从0.2开始试,最后定在0.45。任务完成率提升到81%,方差降低了约40%。

这个提升不算惊天动地,但胜在实现简单、可解释性强。而且PID的输出是连续的,不会像规则引擎那样出现“硬切换”导致的体验断裂。

3. ADRC:当PID不够用时,自抗扰控制怎么救场

PID虽然好用,但它有个根本性缺陷:它假设系统模型是线性的、时不变的。而AI Agent系统恰恰是非线性、时变、强耦合的。工具之间的依赖关系、上下文长度的动态变化、模型版本的更新,都会让系统特性发生漂移。这时候,ADRC(自抗扰控制)就派上用场了。

3.1 ADRC的核心思想:把一切未知扰动都“观测”出来

ADRC的精髓在于扩张状态观测器(ESO)。它把系统内部的不确定性、外部扰动、未建模动态,全部打包成一个“总扰动”,然后用一个观测器实时估计这个总扰动,并在控制量里把它抵消掉。

用Agent的场景来类比:你不需要知道为什么这次工具调用失败了(可能是API限流、可能是网络抖动、可能是模型幻觉),你只需要一个机制能实时估计“当前系统偏离预期有多远”,然后把这个偏差补偿掉。ESO就是这个机制。

ADRC的另一个核心是跟踪微分器(TD),它负责安排过渡过程,让系统从一个状态平滑地过渡到另一个状态,避免超调。在Agent里,这可以理解为“任务规划的平滑过渡”——当用户突然改变需求时,Agent不应该瞬间跳转,而应该有一个合理的过渡策略。

class ADRCController: def __init__(self, b0, omega_o, omega_c): self.b0 = b0 # 系统增益估计 self.omega_o = omega_o # 观测器带宽 self.omega_c = omega_c # 控制器带宽 self.z1 = 0.0 # 状态估计 self.z2 = 0.0 # 扰动估计 self.dt = 1.0 def update(self, y, u): # ESO更新 e = self.z1 - y self.z1 += self.dt * (self.z2 - 2 * self.omega_o * e + self.b0 * u) self.z2 += self.dt * (-self.omega_o**2 * e) # 控制律 u0 = self.omega_c * (0 - self.z1) # 简化版,实际需结合TD u_new = (u0 - self.z2) / self.b0 return u_new

这段代码是ADRC的极简实现。y是系统输出(比如任务完成度),u是控制量(比如Agent的策略调整幅度)。z2就是ESO估计出来的“总扰动”。实际使用时,b0需要根据你的Agent系统特性来估计,一般从1.0开始试,观察系统响应。

3.2 ADRC相比PID的优势在哪里

抗扰动能力更强。PID对扰动的响应是“事后修正”,而ADRC是“实时估计+前馈补偿”。在Agent场景里,这意味着当API延迟突然增大时,ADRC能更快地调整策略,而不是等到任务失败后才开始补救。

对模型不确定性更鲁棒。PID需要你手动调参来适应不同的系统状态,而ADRC的ESO会自动估计系统特性,参数适应性更好。我实测下来,同一个ADRC控制器在不同类型的Agent任务上,表现比PID稳定得多,不需要频繁重新调参。

过渡过程更平滑。TD的引入让ADRC在处理“任务切换”时更自然。比如用户从“查订单”突然切换到“投诉”,ADRC会安排一个平滑的过渡,而不是硬切。

3.3 ADRC在Agent里的落地架构

我目前的架构是这样的:Agent的主循环还是LLM规划+工具执行,但在外面套了一层ADRC控制器。控制器的输入是“任务完成度评分”(用一个轻量级评估模型实时计算),输出是“策略调整信号”。这个信号会以自然语言的形式注入到下一轮的prompt里,比如“当前任务完成度偏低,建议扩大搜索范围”或者“检测到系统扰动增大,建议切换到备用工具”。

这个架构的好处是不侵入原有Agent逻辑。你不需要重写Agent的规划模块,只需要在prompt里加一段动态生成的“控制建议”就行。实测下来,任务完成率的提升在10-15个百分点,而且系统的“行为方差”显著降低。

提示:ADRC的omega_o和omega_c两个带宽参数,一般满足omega_o = 3~5 * omega_c的关系。在Agent场景里,我建议omega_c从0.5开始试,omega_o设为2.0左右。带宽越大,响应越快,但对噪声越敏感。

4. 从PID到ADRC的迁移路径:一个渐进式改造方案

直接上ADRC可能会让系统变得过于复杂,尤其是如果你的Agent还在快速迭代阶段。我建议走一条渐进式的路线:先用PID建立反馈闭环,再逐步引入ADRC的组件。

4.1 第一阶段:加一个简单的P控制器

这个阶段的目标是“让Agent有反馈意识”。具体做法:在每次工具调用后,用一个轻量级评分函数(可以是规则、也可以是小模型)计算“当前结果与预期的偏差”,然后把这个偏差以自然语言的形式注入下一轮prompt。

比如:

def generate_control_hint(deviation): if deviation > 0.7: return "当前结果与预期偏差较大,建议重新规划或更换工具。" elif deviation > 0.3: return "当前结果部分符合预期,建议微调策略后继续。" else: return "当前结果符合预期,继续执行。"

这个阶段的改动量极小,但效果立竿见影。我试过在一个代码生成Agent上加了这个简单的P控制,任务成功率从65%提升到78%。

4.2 第二阶段:引入I项和D项

当P控制跑稳之后,开始加I项和D项。I项用来处理“持续性偏差”,比如某个工具连续多次返回低质量结果。D项用来抑制“策略震荡”,比如Agent在多个工具之间反复横跳。

这个阶段需要引入一个状态缓冲区,记录最近N次的偏差值。N一般取5-10,太小了I项累积不够,太大了响应迟钝。

class AgentPID: def __init__(self, kp=0.4, ki=0.1, kd=0.05, buffer_size=8): self.kp, self.ki, self.kd = kp, ki, kd self.buffer = [] self.buffer_size = buffer_size self.integral = 0.0 def update(self, deviation): self.buffer.append(deviation) if len(self.buffer) > self.buffer_size: self.buffer.pop(0) self.integral += deviation self.integral = max(min(self.integral, 5.0), -5.0) derivative = 0.0 if len(self.buffer) >= 2: derivative = self.buffer[-1] - self.buffer[-2] output = (self.kp * deviation + self.ki * self.integral + self.kd * derivative) return output

4.3 第三阶段:用ESO替换I项

当系统足够稳定后,可以把I项替换成ESO。ESO的优势在于它能估计“总扰动”,而不仅仅是“偏差累积”。这意味着它对突发扰动的响应更快,而且不会像I项那样容易饱和。

这个阶段的迁移成本稍高,需要你定义系统的b0参数。我的经验是:先用PID跑一段时间,记录控制量和系统输出的关系,然后用最小二乘法估计b0。或者更简单粗暴一点,从1.0开始试,观察系统响应,如果震荡就减小,如果迟钝就增大。

4.4 迁移过程中的注意事项

不要一次性替换所有组件。我见过有人直接把PID换成ADRC,结果系统行为完全变了,之前的调参经验全部作废。渐进式迁移的好处是每一步都可回退、可对比。

保留日志和回放能力。Agent的调试比传统控制系统难得多,因为LLM的输出不可复现。一定要记录每次控制的输入、输出、以及Agent的最终行为,方便事后分析。

控制器的输出要“翻译”成自然语言。Agent的下一轮prompt是自然语言,所以控制器的数值输出需要有一个“翻译层”。这个翻译层可以很简单,比如把输出值映射到几个预设的提示模板上。

5. 常见问题与排查技巧实录

这套东西我在三个不同的Agent项目里跑过,踩了不少坑。下面整理成速查表,方便你对照排查。

问题现象可能原因排查方法解决方案
Agent行为剧烈震荡P参数过大或D参数过大记录连续10轮的控制输出,看是否正负交替减小P或D,先只用P跑
Agent反应迟钝P参数过小或I限幅过紧观察任务完成度曲线,是否长时间不变化增大P,放宽I限幅
任务中途放弃I项饱和导致控制量极端检查积分值是否达到限幅边界加积分限幅,或引入抗饱和机制
切换工具后性能下降控制器参数未适配新工具对比切换前后的控制输出分布为不同工具设置不同的参数组
系统对突发扰动无响应ESO带宽过小注入一个阶跃扰动,观察恢复时间增大omega_o
控制建议与Agent行为不一致翻译层映射错误检查控制输出到prompt的映射逻辑简化映射,用离散档位代替连续值

5.1 一个典型的排查案例

有一次,我的Agent在处理多轮对话时,突然开始反复问同一个澄清问题。查日志发现,控制器的I项在连续几轮低偏差后累积到了一个较大的负值,导致控制输出变成了“建议进一步澄清”。但实际上的偏差并不大,只是I项累积过头了。

解决方案是加了一个积分分离逻辑:只有当偏差大于某个阈值时,才累积I项。偏差小的时候,I项不累积。这个改动很小,但解决了问题。

def update(self, deviation, threshold=0.2): if abs(deviation) > threshold: self.integral += deviation # 其余逻辑不变

5.2 另一个坑:控制器和LLM的“节奏”不匹配

LLM的推理有延迟,工具调用也有延迟,而控制器的更新频率如果太高,会导致控制信号“超前”于系统状态。我一开始把控制器的dt设成了1(每轮对话更新一次),但实际上一轮对话可能包含多次工具调用。后来改成“每次工具调用后更新”,效果好很多。

实操心得:控制器的更新频率应该和“系统状态变化频率”匹配。Agent的状态变化发生在每次工具调用后,所以控制器也应该在每次工具调用后更新。不要按时间更新,要按事件更新。

5.3 关于JEV模型的补充说明

最近社区里有人在讨论JEV模型和Agent控制的结合。我理解JEV是一种用于估计系统状态的方法,它的优势在于对非线性系统的估计精度较高。如果你已经在用ADRC,可以把ESO替换成JEV观测器,理论上能进一步提升估计精度。不过JEV的实现复杂度比ESO高不少,建议先把ADRC跑稳再考虑。

6. 一些个人体会和后续可以折腾的方向

这套控制论+Agent的思路,我折腾了大半年,最大的体会是:不要把LLM当成万能药。LLM很擅长“生成”,但不擅长“稳定”。稳定性这件事,交给经典控制理论更靠谱。LLM负责“聪明”,控制器负责“稳”,两者分工明确,系统整体表现反而更好。

后续我打算试几个方向:一是把ADRC的TD组件单独拿出来,用于Agent的任务规划平滑过渡;二是探索一下“多Agent系统”里的分布式控制,每个Agent有自己的局部控制器,再加一个全局协调器;三是把控制器的参数整定自动化,用贝叶斯优化或者强化学习来调参,减少人工介入。

最后分享一个小技巧:如果你觉得ADRC太复杂,可以先从“纯P控制+积分分离”开始。这个组合实现简单,但能解决80%的Agent稳定性问题。等这套跑稳了,再逐步引入ESO和TD。不要一上来就追求“完整版ADRC”,那样调试成本太高,容易劝退。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询