在基于大模型的Agent执行多步任务时,一个常见且令人头疼的问题是动作重复。Agent可能不断调用同一个API、重复查询相同的数据库表,或者反复输出同一段文本,导致任务无法推进,最终超时或失败。从技术角度看,这种重复调用往往源于两个因素:一是Agent观测到的状态(或上下文)没有发生明显变化,策略认为当前动作仍然是最优;二是动作选择过程缺少足够的随机性或多样性约束,模型在局部最优解上“卡死”。本文将分别介绍历史去重和行动多样性约束两类方法,通过代码示例说明如何实现,并讨论在实际Agent框架中落地时的注意事项。

历史去重:用滑动窗口直接阻断重复动作
历史去重是最直接的工程手段:记录Agent最近执行过的动作或访问过的状态,当新一轮决策即将重复同样的动作时,强制从候选列表中剔除该动作,从而迫使Agent选择其他选项。这种思路类似于强化学习中的“动作掩码”,但实现上更加轻量,不需要修改模型参数。
动作去重通常使用一个固定大小的队列(滑动窗口)保存最近N次执行的动作。每次Agent要输出一个新动作时,检查该动作是否已经在窗口中,如果是则将其从合法动作集合中排除。窗口大小N的选择很关键:太小则无法有效避免循环,太大会导致某些必要动作长时间被禁用。一个实用的经验值是取动作空间大小的1/3到1/2之间,并结合具体任务进行调试。例如对于有10个工具可用的Agent,窗口大小设为4~5通常能显著减少重复,又不会过度限制正常的多步调用。
下面是一个用Python实现的动作去重包装器的例子,它包裹Agent的动作选择函数,通过collections.deque维护一个固定长度的历史记录,并在每次决策前过滤掉近期重复项。
from collections import deque
class ActionDeduplicator:
def __init__(self, action_space, window_size=5):
self.action_space = list(action_space) # 所有可行动作
self.window_size = window_size
self.history = deque(maxlen=window_size)
def filter_actions(self):
# 返回当前允许执行的动作列表
if len(self.history) == 0:
return self.action_space.copy()
recent = set(self.history)
allowed = [a for a in self.action_space if a not in recent]
# 如果所有动作都被去重,则放宽限制,只排除最近的一个
if not allowed:
allowed = [a for a in self.action_space if a != self.history[-1]]
return allowed
def record_action(self, action):
self.history.append(action)
# 使用示例
dedup = ActionDeduplicator(action_space=['search', 'click', 'type', 'scroll'], window_size=3)
allowed = dedup.filter_actions()
print(allowed) # 第一次全部允许
dedup.record_action('search')
dedup.record_action('click')
allowed = dedup.filter_actions()
print(allowed) # search和click被排除,剩下type和scroll
除了动作级别的去重,还可以基于状态或上下文进行去重。例如Agent每轮都会将当前的观察和已执行的动作拼接后送入模型,如果拼接结果与之前某轮完全相同,说明Agent进入了状态循环。此时可以主动在上下文中插入一个提示,或者直接随机选择一个不同的动作。状态去重的实现通常使用一个哈希表存储最近若干轮的上下文指纹,计算代价低,适合记忆较长的场景。
行动多样性约束:从策略层面注入探索动力
历史去重是一种硬性过滤,可能在某些情况下过于粗暴——例如Agent确实需要重复调用同一个工具(比如多次翻页或者多次查询相同接口但参数不同)。此时更好的办法是在策略优化或决策生成过程中加入多样性约束,让模型自然地倾向于选择不同动作,而不是简单地禁止重复。
在强化学习领域,最大熵方法(如Soft Actor-Critic)通过在奖励函数中加入动作分布的熵项来鼓励探索。对于基于LLM的Agent,我们可以在生成动作时使用更高的温度参数,或者在提示词中明确要求“避免与之前的动作重复”。更工程化的做法是在微调Agent策略时,定义一个多样性正则项,将其加入损失函数。具体来说,如果Agent输出一个动作概率分布,我们计算该分布的熵,熵越大代表动作越分散,将熵的负值(或乘以一个系数)加到损失中,就能推动模型产生更多样化的动作。
下面是一个PyTorch示例,展示如何在一个简单的策略网络中计算多样性损失,并将其与任务损失结合。
import torch
import torch.nn as nn
import torch.nn.functional as F
class PolicyWithDiversity(nn.Module):
def __init__(self, input_dim, action_dim):
super().__init__()
self.fc = nn.Linear(input_dim, action_dim)
def forward(self, state):
logits = self.fc(state)
probs = F.softmax(logits, dim=-1)
return probs
# 假设已经有一个batch的状态
states = torch.randn(8, 32) # batch_size=8, feature_dim=32
policy = PolicyWithDiversity(input_dim=32, action_dim=10)
probs = policy(states)
# 计算动作分布的熵
log_probs = torch.log(probs + 1e-8)
entropy = -(probs * log_probs).sum(dim=-1).mean()
# 任务损失(例如监督信号或环境奖励的负值)
# 这里用随机值代替真实的任务损失
task_loss = torch.tensor(0.5, requires_grad=True)
# 多样性系数 lambda_div 控制多样性项的强度
lambda_div = 0.1
total_loss = task_loss - lambda_div * entropy # 最大化熵等价于最小化负熵
total_loss.backward()
print(f"Entropy: {entropy.item():.4f}, Total loss: {total_loss.item():.4f}")
在实际的LLM Agent场景中,我们并不总是能直接训练策略网络,更多时候是通过提示工程和采样策略来提升多样性。例如在生成下一步action时,可以要求模型给出多个候选动作,然后根据与历史动作的相似度打分,优先选择最不相似的那个。也可以使用温度采样(temperature sampling)和top-k采样,增加低概率动作被选中的机会。这些方法虽然简单,但在很多循环问题上效果显著。
工程落地的权衡与调优
将历史去重和多样性约束同时应用时,需要仔细调节参数以避免副作用。去重窗口过大可能阻止Agent完成一些需要重复动作的任务,比如一个网页自动化Agent需要连续点击“下一页”10次,如果窗口大小为10,那么第2次点击就被禁止了。窗口过小则无法打破短周期循环。一个实用的策略是设置去重窗口的动态调整:如果检测到Agent在短时间内连续重复同一动作超过阈值,则临时扩大窗口或强制随机选择;如果任务允许重复动作,则降低去重强度。
多样性约束的权重(如熵系数lambda_div)也需要根据任务类型调整。对于探索性强的任务(如信息收集、环境探索),可以设置较高的系数;而对于需要严格遵守步骤的确定性任务(如数据录入、固定流程执行),过高的多样性反而会导致偏离目标。在提示词中要求模型“避免重复”时,也要注意措辞,例如“除非任务明确要求重复,否则不要连续执行相同的动作”,这样既保留了灵活性又限制了无意义的循环。
在LangChain、AutoGPT等主流Agent框架中,这些机制可以很方便地集成。例如在LangChain的AgentExecutor中,可以通过自定义AgentOutputParser来过滤重复动作,或者在PromptTemplate中加入历史动作摘要,让模型自己感知到已经执行过哪些操作。还有些框架支持回调机制,可以在每次动作执行后检查是否与历史重复,如果重复则向模型反馈一条负面信号,促使其调整策略。无论采用哪种方式,核心思想都是让Agent的决策过程具有“记忆力”和“新鲜感”,从而高效地完成多步任务。
总结来说,历史去重通过硬性拦截来快速阻断重复,而行动多样性约束通过软性引导来鼓励探索。两者结合能够显著减少Agent在任务中陷入死循环的概率,提升整体执行效率。开发者在实际项目中应根据任务特性和Agent表现灵活调整去重窗口与多样性权重,必要时加入人工监督信号来进一步指导模型输出。