Skip to content

fix(agent): add configurable repeated-reply convergence guard#6921

Open
Jacobinwwey wants to merge 2 commits intoAstrBotDevs:masterfrom
Jacobinwwey:fix/repeat-llm-reply-guard
Open

fix(agent): add configurable repeated-reply convergence guard#6921
Jacobinwwey wants to merge 2 commits intoAstrBotDevs:masterfrom
Jacobinwwey:fix/repeat-llm-reply-guard

Conversation

@Jacobinwwey
Copy link

@Jacobinwwey Jacobinwwey commented Mar 25, 2026

Motivation / 动机

在部分异常任务路径中,Agent 可能连续多步输出几乎完全相同的文本(用户未要求重复),造成对话噪声和“看起来像卡死”的体验。
当前虽然有 max_agent_step 全局上限,但在上限较高时,重复输出仍会持续较久。

本 PR 增加一个轻量、可配置的“连续相同回复收敛保护”,用于更早阻断这类重复循环。

Modifications / 改动点

核心行为增强:

  • astrbot/core/astr_agent_run_util.py
    • 新增连续相同回复签名检测(按 llm_result 文本签名计数)。
    • 当连续重复达到阈值时,触发收敛保护:
      • 禁用当前 run 的工具调用;
      • 注入收敛提示,要求直接给出最终答复;
      • 跳转到与 max-step 一致的收敛路径,避免持续重复。
    • 保护阈值可配置,支持 0 关闭。

配置项新增:

  • astrbot/core/config/default.py
    • 新增 provider_settings.repeat_reply_guard_threshold(默认 3)。
    • 新增 schema 与 WebUI metadata 描述,支持可视化配置。

调用链透传:

  • astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py
    • 初始化并规范化读取 repeat_reply_guard_threshold
    • 将阈值透传到 run_agent / run_live_agent

测试覆盖:

  • 新增 tests/unit/test_astr_agent_run_util.py

    • 覆盖“阈值触发后收敛”路径。
    • 覆盖“阈值为 0 时关闭保护”路径。
  • This is NOT a breaking change. / 这不是一个破坏性变更。

Overlap Check / 业务重叠检查

已检查当前 author:Jacobinwwey 的 open PR:

  • #6872 关注 subagent handoff 调用次数限制;
  • #6259 关注 tool result dedup 与 tool error loop guard。

本 PR 聚焦 LLM 文本输出层 的“连续相同回复收敛”,不改 handoff 次数护栏与 tool-result dedup 语义,业务目标独立。

Test Results / 测试结果

本地执行:

python -m py_compile astrbot/core/astr_agent_run_util.py \
  astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py \
  astrbot/core/config/default.py \
  tests/unit/test_astr_agent_run_util.py
ruff check astrbot/core/astr_agent_run_util.py \
  astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py \
  astrbot/core/config/default.py \
  tests/unit/test_astr_agent_run_util.py
PYTHONPATH=. pytest -q tests/unit/test_astr_agent_run_util.py \
  tests/test_tool_loop_agent_runner.py::test_max_step_limit_functionality

结果:

  • py_compile: passed
  • ruff check: All checks passed
  • pytest: 3 passed

Checklist / 检查清单

  • 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
  • 👀 我的更改经过了良好的测试,并已在上方提供了"验证步骤"和"运行结果"。/ My changes have been well-tested, and "Verification Steps" and "Results" have been provided above.
  • 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 requirements.txtpyproject.toml 文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
  • 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code.

🤖 Generated with Codex

Summary by Sourcery

Add a configurable guard to detect and converge repeated agent replies, wiring it through the agent pipeline and configuration with accompanying tests.

New Features:

  • Introduce a repeat-reply convergence guard in the agent runner that detects consecutively identical LLM responses and forces a final answer instead of continued looping.
  • Expose a configurable repeat_reply_guard_threshold setting in provider settings, including defaults and UI metadata, to control or disable the guard.

Enhancements:

  • Add shared utilities for normalizing repeat-reply guard thresholds across runtime usage and configuration, ensuring consistent handling of invalid values.
  • Propagate the repeat-reply guard threshold through agent pipeline stages and live agent execution so the protection applies uniformly in both modes.

Tests:

  • Add unit tests covering repeat-reply guard behavior, including normal operation, convergence triggering, and disabling via zero threshold.
  • Add tests for threshold normalization logic and for keeping default configuration and function signatures in sync for the repeat-reply guard.

@auto-assign auto-assign bot requested review from advent259141 and anka-afk March 25, 2026 05:54
@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Mar 25, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求旨在解决 Agent 在某些异常任务路径中可能连续输出相同文本,导致对话噪声和用户体验不佳的问题。通过引入一个轻量且可配置的“连续相同回复收敛保护”机制,Agent 能够在检测到连续重复回复达到预设阈值时,主动停止重复行为,禁用工具调用,并被引导直接给出最终答复,从而有效避免无限循环,提升 Agent 的稳定性和用户交互质量。

Highlights

  • 核心行为增强: 在 astrbot/core/astr_agent_run_util.py 中新增了连续相同回复签名检测机制,当连续重复次数达到可配置阈值时,将触发收敛保护,禁用工具调用并注入收敛提示,引导 Agent 直接给出最终答复。
  • 配置项新增: 在 astrbot/core/config/default.py 中新增了 provider_settings.repeat_reply_guard_threshold 配置项(默认值为 3),并为其添加了 schema 和 WebUI metadata,支持可视化配置。
  • 调用链透传: 在 astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py 中实现了 repeat_reply_guard_threshold 的初始化、规范化读取以及向 run_agent / run_live_agent 函数的透传。
  • 测试覆盖: 新增了 tests/unit/test_astr_agent_run_util.py 单元测试文件,覆盖了阈值触发收敛和阈值为 0 时关闭保护的两种场景。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@dosubot dosubot bot added the area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. label Mar 25, 2026
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The normalization logic for repeat_reply_guard_threshold is duplicated between _normalize_repeat_reply_guard_threshold in astr_agent_run_util.py and the initialize method in internal.py; consider reusing the helper (or centralizing this logic) to keep behavior consistent and easier to maintain.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The normalization logic for `repeat_reply_guard_threshold` is duplicated between `_normalize_repeat_reply_guard_threshold` in `astr_agent_run_util.py` and the `initialize` method in `internal.py`; consider reusing the helper (or centralizing this logic) to keep behavior consistent and easier to maintain.

## Individual Comments

### Comment 1
<location path="astrbot/core/astr_agent_run_util.py" line_range="90-95" />
<code_context>
     return status_msg


+def _normalize_repeat_reply_guard_threshold(value: int) -> int:
+    try:
+        parsed = int(value)
+    except (TypeError, ValueError):
+        return 0
+    return max(0, parsed)
+
+
</code_context>
<issue_to_address>
**suggestion:** Consider reusing this normalization logic where the threshold is parsed in the pipeline stage to avoid divergence.

`internal.py.initialize` manually coerces `repeat_reply_guard_threshold` to a non‑negative int using logic that overlaps with `_normalize_repeat_reply_guard_threshold`. To avoid drift and make future changes safer, consider using a single normalization helper (or moving this one to a shared module) so both call sites share the same coercion rules.

Suggested implementation:

```python
def normalize_repeat_reply_guard_threshold(value) -> int:

```

```python
def normalize_repeat_reply_guard_threshold(value) -> int:
    try:
        parsed = int(value)
    except (TypeError, ValueError):
        return 0
    return max(0, parsed)

```

To fully implement the suggestion, you should also:

1. In `astrbot/core/internal.py` (or wherever `initialize` is defined and the pipeline stage parses `repeat_reply_guard_threshold`):
   - Import the helper:
     - `from astrbot.core.astr_agent_run_util import normalize_repeat_reply_guard_threshold`
   - Replace the manual coercion logic for `repeat_reply_guard_threshold` with:
     - `repeat_reply_guard_threshold = normalize_repeat_reply_guard_threshold(repeat_reply_guard_threshold)`
2. Remove any now-redundant bounds checking or `int()` parsing around `repeat_reply_guard_threshold` in that initializer to ensure there is a single source of truth for this normalization.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a 'repeat reply guard' mechanism to prevent agents from getting stuck in loops by repeatedly outputting the same LLM result. It adds a repeat_reply_guard_threshold parameter to run_agent and related functions, along with configuration in default.py and its schema. New helper functions _normalize_repeat_reply_guard_threshold and _build_chain_signature are added, and the run_agent logic is updated to implement the guard. Unit tests for this new functionality are also included. The review comments suggest that the normalization logic for repeat_reply_guard_threshold is duplicated and can be simplified, specifically recommending the removal of the _normalize_repeat_reply_guard_threshold function in astr_agent_run_util.py and a more concise implementation of the normalization within InternalAgentSubStage.initialize.

Comment on lines +90 to +95
def _normalize_repeat_reply_guard_threshold(value: int) -> int:
try:
parsed = int(value)
except (TypeError, ValueError):
return 0
return max(0, parsed)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这个 _normalize_repeat_reply_guard_threshold 标准化函数是多余的。因为 repeat_reply_guard_threshold 参数在传入 run_agent 之前,已经在 InternalAgentSubStage.initialize 中被处理过了。为了避免代码重复并简化逻辑,建议移除此函数。

Comment on lines +117 to +119
guard_threshold = _normalize_repeat_reply_guard_threshold(
repeat_reply_guard_threshold
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

由于 repeat_reply_guard_threshold 的值在传入此函数前已经经过了标准化处理,因此这里对 _normalize_repeat_reply_guard_threshold 的调用是不必要的。你可以直接进行赋值。

    guard_threshold = repeat_reply_guard_threshold

Comment on lines +70 to +77
if isinstance(self.repeat_reply_guard_threshold, bool):
self.repeat_reply_guard_threshold = 3
try:
self.repeat_reply_guard_threshold = int(self.repeat_reply_guard_threshold)
except (TypeError, ValueError):
self.repeat_reply_guard_threshold = 3
if self.repeat_reply_guard_threshold < 0:
self.repeat_reply_guard_threshold = 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这部分用于规范化 repeat_reply_guard_threshold 的逻辑有些冗长,可以简化以提高可读性。

Suggested change
if isinstance(self.repeat_reply_guard_threshold, bool):
self.repeat_reply_guard_threshold = 3
try:
self.repeat_reply_guard_threshold = int(self.repeat_reply_guard_threshold)
except (TypeError, ValueError):
self.repeat_reply_guard_threshold = 3
if self.repeat_reply_guard_threshold < 0:
self.repeat_reply_guard_threshold = 0
try:
if isinstance(self.repeat_reply_guard_threshold, bool):
raise TypeError
parsed_val = int(self.repeat_reply_guard_threshold)
self.repeat_reply_guard_threshold = max(0, parsed_val)
except (TypeError, ValueError):
self.repeat_reply_guard_threshold = 3

@Jacobinwwey
Copy link
Author

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The normalization logic for repeat_reply_guard_threshold in internal.initialize duplicates the new _normalize_repeat_reply_guard_threshold helper; consider reusing the helper there (or centralizing normalization in one place) to keep behavior consistent and easier to maintain.
  • The injected convergence prompt string in the guard is currently hard-coded in Chinese inside run_agent; if you expect multilingual deployments, it may be worth routing this through your existing i18n/config mechanism or at least centralizing the message text for easier adjustment.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The normalization logic for `repeat_reply_guard_threshold` in `internal.initialize` duplicates the new `_normalize_repeat_reply_guard_threshold` helper; consider reusing the helper there (or centralizing normalization in one place) to keep behavior consistent and easier to maintain.
- The injected convergence prompt string in the guard is currently hard-coded in Chinese inside `run_agent`; if you expect multilingual deployments, it may be worth routing this through your existing i18n/config mechanism or at least centralizing the message text for easier adjustment.

## Individual Comments

### Comment 1
<location path="astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py" line_range="67-76" />
<code_context>
             self.tool_schema_mode = "full"
         if isinstance(self.max_step, bool):  # workaround: #2622
             self.max_step = 30
+        self.repeat_reply_guard_threshold: int = settings.get(
+            "repeat_reply_guard_threshold", 3
+        )
+        if isinstance(self.repeat_reply_guard_threshold, bool):
+            self.repeat_reply_guard_threshold = 3
+        try:
+            self.repeat_reply_guard_threshold = int(self.repeat_reply_guard_threshold)
+        except (TypeError, ValueError):
+            self.repeat_reply_guard_threshold = 3
+        if self.repeat_reply_guard_threshold < 0:
+            self.repeat_reply_guard_threshold = 0
         self.show_tool_use: bool = settings.get("show_tool_use_status", True)
</code_context>
<issue_to_address>
**suggestion:** Repeat-reply threshold normalization is duplicated instead of reusing the shared helper.

This normalization logic (bool handling, int cast, clamping negatives) closely matches `_normalize_repeat_reply_guard_threshold` in `astr_agent_run_util.py`. To keep behavior consistent and avoid future drift, please refactor to reuse that helper here, or extract a shared utility that both call into.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Jacobinwwey
Copy link
Author

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The normalization logic for repeat_reply_guard_threshold is duplicated between _normalize_repeat_reply_guard_threshold in astr_agent_run_util.py and the initialization in agent_sub_stages/internal.py; consider reusing the helper or centralizing this normalization to avoid divergent behavior on edge inputs.
  • repeat_reply_guard_threshold's default value 3 is hard-coded in several places (config default, provider template, stage initialize, and the run_agent / run_live_agent signatures); it would be more maintainable to derive these from a single configuration source or constant to prevent future inconsistencies.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The normalization logic for `repeat_reply_guard_threshold` is duplicated between `_normalize_repeat_reply_guard_threshold` in `astr_agent_run_util.py` and the initialization in `agent_sub_stages/internal.py`; consider reusing the helper or centralizing this normalization to avoid divergent behavior on edge inputs.
- `repeat_reply_guard_threshold`'s default value `3` is hard-coded in several places (config default, provider template, stage initialize, and the `run_agent` / `run_live_agent` signatures); it would be more maintainable to derive these from a single configuration source or constant to prevent future inconsistencies.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Jacobinwwey
Copy link
Author

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The injected convergence prompt in run_agent is hard-coded Chinese text; consider routing this through your existing localization/prompting mechanism or at least aligning its language with the conversation/user locale.
  • The chain signature for repeat detection is based on get_plain_text(with_other_comps_mark=True) with whitespace normalized; if messages can contain dynamic or non-deterministic components (timestamps, IDs, etc.), consider whether they should be stripped or normalized further to avoid missing genuinely repeated replies.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The injected convergence prompt in `run_agent` is hard-coded Chinese text; consider routing this through your existing localization/prompting mechanism or at least aligning its language with the conversation/user locale.
- The chain signature for repeat detection is based on `get_plain_text(with_other_comps_mark=True)` with whitespace normalized; if messages can contain dynamic or non-deterministic components (timestamps, IDs, etc.), consider whether they should be stripped or normalized further to avoid missing genuinely repeated replies.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Jacobinwwey
Copy link
Author

“硬编码中文提示词”和“重复检测签名是否要进一步归一化”,都不属于必须处理的新增 unresolved 线程;而且在没有失败测试、maintainer 明确要求或生产风险的新证据前,也不应再次切换同一 concern 的策略。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant