From b8008da9597916194d2fc695006a156369748d02 Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Thu, 22 Oct 2026 23:01:40 +0800 Subject: [PATCH] feat(config): configurable conversation history window (B3) FastGPT maintains full history server-side; LangGraph truncated the injected history at a hardcoded 50 messages, so long conversations behaved differently across backends. - Config.AGENT_HISTORY_MESSAGES (env): max messages injected into the model prompt (recent N, both roles counted); default 50 unchanged; larger values support long conversations; 0 = no truncation, matching FastGPT's full-history semantics. Only the injected window is affected - dialogue stays fully persisted in the checkpoint. - validate(): rejects negative values; 0 and positive accepted - conversation_history reads the config (import-time class attribute, monkeypatched in tests); explicit None-check keeps 0 falsy-safe - .env.example documents the knob Verified: 225 unit tests (6 new history-window tests) --- .env.example | 3 ++ src/agent/collect.py | 21 +++++++++-- src/core/config.py | 7 ++++ test/agent/test_history_window.py | 62 +++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 test/agent/test_history_window.py diff --git a/.env.example b/.env.example index 54423c1..3539ca8 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,9 @@ VOICE_CONFIG=config/voice.json # 如思考开关 KIMI 网关为 {"reasoning_effort":"none"})。 # 超时/重试缺省 60 秒 / 3 次,与 FastGPT 客户端 SLA 对齐 # (契约测试 test/backends/test_sla_alignment.py)。 +# AGENT_HISTORY_MESSAGES=50:注入模型的对话历史上限(最近 N 条, +# 双向计数);调大支持长对话;0 = 不截断(对齐 FastGPT 全量历史)。 +# AGENT_HISTORY_MESSAGES=0 # LLM_PROVIDERS=deepseek,kimi,gpt6luna # LLM_PROVIDER=deepseek # LLM_DEEPSEEK_BASE_URL=https://api.openai.com/v1 diff --git a/src/agent/collect.py b/src/agent/collect.py index 950abda..64890b5 100644 --- a/src/agent/collect.py +++ b/src/agent/collect.py @@ -108,13 +108,26 @@ async def validated_reply( raise CollectReplyError("模型回复格式、阶段转换或时间依据不合规(一次修复后仍失败)") -# 工作流 chatNode 的 history 配置为 50;保留独立消息及其角色。 -_HISTORY_MESSAGES = 50 +# 工作流 chatNode 的 history 配置为 50;默认对齐(Config. +# AGENT_HISTORY_MESSAGES,env 可调大,0 = 不截断对齐 FastGPT 全量语义)。 +_HISTORY_MESSAGES_DEFAULT = 50 def conversation_history(state: AgentState) -> list[dict[str, str]]: - """历史不拼入 user 消息,不剥除历史 assistant 的状态前缀。""" - return [dict(message) for message in (state.get("dialogue") or [])[-_HISTORY_MESSAGES:]] + """历史不拼入 user 消息,不剥除历史 assistant 的状态前缀。 + + 截断窗口取 Config.AGENT_HISTORY_MESSAGES(import 时固化的类属性, + 测试用 monkeypatch 调整);0 或负数视为不截断。 + """ + from ..core.config import Config + + limit = getattr(Config, "AGENT_HISTORY_MESSAGES", None) + if limit is None: + limit = _HISTORY_MESSAGES_DEFAULT + dialogue = state.get("dialogue") or [] + if limit > 0: + dialogue = dialogue[-limit:] + return [dict(message) for message in dialogue] def context_prompt(state: AgentState, *, time_context: dict | None = None) -> str: diff --git a/src/core/config.py b/src/core/config.py index 130069a..cee3594 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -33,6 +33,11 @@ class Config: # 运行时选择:LLM_PROVIDER=<名>;缺省或 fake = 离线脚本模型。 LLM_PROVIDERS = os.getenv("LLM_PROVIDERS", "") LLM_PROVIDER = os.getenv("LLM_PROVIDER", "") + # 对话历史注入模型时的最大消息条数(最近 N 条,双向计数)。 + # 默认 50(约 25 轮);调大支持长对话;0 = 不截断(与 FastGPT + # 服务端全量历史语义对齐)。对话正文仍随 checkpoint 全量保存, + # 本参数只影响注入提示词的历史窗口。 + AGENT_HISTORY_MESSAGES = int(os.getenv("AGENT_HISTORY_MESSAGES", "50")) # LLM 客户端是否信任系统代理(Windows 注册表)。缺省 False: # 本地网关(127.0.0.1)必须直连;外部网关若需代理设为 true。 LLM_HTTP_TRUST_ENV = os.getenv("LLM_HTTP_TRUST_ENV", "false").lower() in ( @@ -149,6 +154,8 @@ class Config: f"Unknown LLM_PROVIDER {cls.LLM_PROVIDER!r}; " f"configured: {sorted(profiles)}" ) + if cls.AGENT_HISTORY_MESSAGES < 0: + raise ValueError("AGENT_HISTORY_MESSAGES must be >= 0 (0 = 不截断)") if cls.AGENT_BACKEND != "fastgpt": return required = [ diff --git a/test/agent/test_history_window.py b/test/agent/test_history_window.py new file mode 100644 index 0000000..300ab0c --- /dev/null +++ b/test/agent/test_history_window.py @@ -0,0 +1,62 @@ +"""对话历史窗口配置测试(B3):env 可调最大注入条数,0 = 不截断。 + +对照 FastGPT:服务端全量维护历史、客户端只发单条消息; +LangGraph 侧历史注入曾硬编码截断 50 条,超长会话两端行为不对称。 +现由 Config.AGENT_HISTORY_MESSAGES 控制(默认 50 不变)。 +""" + +import pytest + +from src.agent.collect import conversation_history +from src.core.config import Config + + +def _dialogue(n: int) -> dict: + return {"dialogue": [ + {"role": "user", "content": f"第{i}条"} for i in range(n) + ]} + + +def test_default_window_keeps_last_50(monkeypatch): + """缺省 50:与工作流 chatNode history 配置一致(现状不变)。""" + monkeypatch.setattr(Config, "AGENT_HISTORY_MESSAGES", 50) + history = conversation_history(_dialogue(60)) + assert len(history) == 50 + assert history[0]["content"] == "第10条" + + +def test_large_window_supports_long_dialogues(monkeypatch): + """调大窗口:长对话注入更多历史(B3 目标)。""" + monkeypatch.setattr(Config, "AGENT_HISTORY_MESSAGES", 500) + history = conversation_history(_dialogue(120)) + assert len(history) == 120 + + +def test_zero_window_disables_truncation(monkeypatch): + """0 = 不截断:对齐 FastGPT 全量历史语义。""" + monkeypatch.setattr(Config, "AGENT_HISTORY_MESSAGES", 0) + history = conversation_history(_dialogue(300)) + assert len(history) == 300 + assert history[0]["content"] == "第0条" + + +def test_history_does_not_mutate_state(monkeypatch): + monkeypatch.setattr(Config, "AGENT_HISTORY_MESSAGES", 2) + state = _dialogue(10) + conversation_history(state) + assert len(state["dialogue"]) == 10 # 原文不动,截断只发生在拷贝 + + +def test_validate_rejects_negative_window(monkeypatch): + monkeypatch.setattr(Config, "AGENT_HISTORY_MESSAGES", -1) + with pytest.raises(ValueError, match="AGENT_HISTORY_MESSAGES"): + Config.validate() + + +def test_validate_accepts_zero_and_positive(monkeypatch): + monkeypatch.setattr(Config, "AGENT_HISTORY_MESSAGES", 0) + monkeypatch.setattr(Config, "AGENT_BACKEND", "langgraph") + monkeypatch.setattr(Config, "LLM_PROVIDER", "fake") + Config.validate() + monkeypatch.setattr(Config, "AGENT_HISTORY_MESSAGES", 10) + Config.validate()