Files
countbot-ai__countbot/tests/test_prompt_cache_stability.py
RIC f58446cae4 修复(Agent): 移除系统提示词中的动态内容以稳定 prompt cache 前缀
- _get_identity() 仅保留配置驱动的静态身份文本,不再注入当前时间与用户称呼/地址/输出语言

- session_summary、channel/chat_id/account_id 不再拼入 system prompt,避免前缀随会话变化

- 新增 _build_dynamic_user_context(),将上述信息类动态内容统一注入首条 user 消息

- team_reminder 保持注入 system 消息的既有行为(指令类内容,独立议题)

- 新增 tests/test_prompt_cache_stability.py 回归测试
2026-09-03 21:27:10 +08:00

208 lines
7.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""回归测试:#113 系统提示词混入动态内容会破坏 prompt caching 前缀稳定性。
覆盖点(对应 issue 验收标准):
1. build_system_prompt() 返回的 system 提示词不含任何信息类动态内容
(无“当前时间”、无用户称呼/地址/输出语言)。
2. build_messages()[0] 是纯静态 system 消息;动态内容
now / 用户信息 / session_summary / channel / chat_id / account_id
全部出现在 user 消息中。
3. team_reminder@ 团队提醒)维持注入 system 消息的既有行为(本次未动,独立议题)。
4. 历史消息顺序、附件路径提示、人格切换默认值等既有行为不回归。
测试通过子类覆盖 DB 相关私有方法,运行无需数据库与环境变量。
"""
import sys
from datetime import datetime
from pathlib import Path
from types import SimpleNamespace
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from backend.modules.agent.context import ContextBuilder # noqa: E402
SESSION_SUMMARY = "S1-summary-会话摘要"
CHANNEL = "feishu"
CHAT_ID = "oc_12345"
ACCOUNT_ID = "acc-999"
PERSONA = SimpleNamespace(
ai_name="小智",
user_name="张总",
user_address="上海",
output_language="英文",
personality="professional",
custom_personality="",
)
class NoDBContextBuilder(ContextBuilder):
"""绕过 DB 访问的 ContextBuilder数据库相关私有方法在单测中打桩"""
def _get_personality_from_db(self, personality_id: str, custom_text: str = "") -> str:
return f"性格描述:{personality_id}"
def _get_active_teams_section(self) -> str:
return ""
def _get_active_team_names(self):
return []
def make_builder(tmp_path, **kwargs) -> NoDBContextBuilder:
return NoDBContextBuilder(workspace=Path(tmp_path), **kwargs)
def full_kwargs(tmp_path):
return dict(
history=[
{"role": "user", "content": "上一轮问题"},
{"role": "assistant", "content": "上一轮回答"},
],
current_message="帮我总结本周工作进展",
session_summary=SESSION_SUMMARY,
media=["/tmp/ctx-attachment-notes.txt"],
channel=CHANNEL,
chat_id=CHAT_ID,
account_id=ACCOUNT_ID,
persona_config=PERSONA,
)
# ---------------------------------------------------------------------------
# 1) build_system_prompt() 为纯静态
# ---------------------------------------------------------------------------
def test_build_system_prompt_has_no_dynamic_content(tmp_path):
builder = make_builder(tmp_path)
for persona in (None, PERSONA):
sys_prompt = builder.build_system_prompt(persona_config=persona)
assert "当前时间" not in sys_prompt
assert "用户称呼" not in sys_prompt
assert "用户常用地址" not in sys_prompt
assert "默认输出语言" not in sys_prompt
# 静态锚点仍应保留
assert "# 核心身份" in sys_prompt
assert "运行环境: " in sys_prompt
# ---------------------------------------------------------------------------
# 2) messages[0] 纯静态,动态内容全部落在 user 消息
# ---------------------------------------------------------------------------
def test_messages_system_static_and_dynamic_in_user(tmp_path):
builder = make_builder(tmp_path)
messages = builder.build_messages(**full_kwargs(tmp_path))
assert messages[0]["role"] == "system"
system_content = messages[0]["content"]
user_content = messages[-1]["content"]
# system 消息不含任何动态内容
assert "当前时间" not in system_content
assert "用户称呼" not in system_content
assert SESSION_SUMMARY not in system_content
assert CHAT_ID not in system_content
assert ACCOUNT_ID not in system_content
# 动态内容全部出现在 user 消息
assert "## 当前会话上下文" in user_content
assert "当前时间" in user_content
assert "用户称呼: 张总" in user_content
assert "用户常用地址: 上海" in user_content
assert "默认输出语言: 英文" in user_content
assert f"## Current Session Context\n{SESSION_SUMMARY}" in user_content
assert f"Channel: {CHANNEL}" in user_content
assert f"Chat ID: {CHAT_ID}" in user_content
assert f"Account ID: {ACCOUNT_ID}" in user_content
def test_system_message_stable_across_time(monkeypatch, tmp_path):
"""相隔一段时间的两次构建system 完全一致、user 消息中的时间随之更新。
用固定时间打桩替代真实 sleep保持单测快速且确定性
真实环境下的跨分钟稳定性在 PR 报告中以 61 秒实测补充说明。
"""
import backend.modules.agent.context as ctx_mod
t0 = datetime(2026, 9, 3, 10, 30, 5)
t1 = datetime(2026, 9, 3, 10, 35, 5)
fixed_times = iter([t0, t1])
monkeypatch.setattr(
ctx_mod,
"datetime",
SimpleNamespace(now=lambda: next(fixed_times)),
)
builder = make_builder(tmp_path)
kwargs = full_kwargs(tmp_path)
first = builder.build_messages(**kwargs)
second = builder.build_messages(**kwargs)
assert first[0]["role"] == "system" and second[0]["role"] == "system"
assert first[0]["content"] == second[0]["content"]
user_first = first[-1]["content"]
user_second = second[-1]["content"]
time_first = t0.strftime("%Y-%m-%d %H:%M (%A)")
time_second = t1.strftime("%Y-%m-%d %H:%M (%A)")
assert time_first != time_second
assert f"- 当前时间: {time_first}" in user_first
assert f"- 当前时间: {time_second}" in user_second
def test_default_persona_user_info_in_user_message(tmp_path):
"""无 persona 配置时,用户资料回退默认值且不泄漏进 system 消息。"""
builder = make_builder(tmp_path)
messages = builder.build_messages(
history=[], current_message="hi", persona_config=None
)
user_content = messages[-1]["content"]
assert "用户称呼: 老板" in user_content
assert "默认输出语言: 中文" in user_content
assert "用户称呼: 老板" not in messages[0]["content"]
# ---------------------------------------------------------------------------
# 3) team_reminder 维持既有行为(本次未动,独立议题)
# ---------------------------------------------------------------------------
def test_team_reminder_still_injected_into_system_message(tmp_path):
builder = make_builder(tmp_path)
messages = builder.build_messages(
history=[],
current_message="大家看看 @数据分析 这个需求怎么拆",
persona_config=PERSONA,
)
assert "检测到 @ 符号" in messages[0]["content"]
assert "可用团队" not in messages[-1]["content"]
# ---------------------------------------------------------------------------
# 4) 历史顺序 / 附件路径 / 人格切换等行为不回归
# ---------------------------------------------------------------------------
def test_history_order_and_attachment_path_preserved(tmp_path):
builder = make_builder(tmp_path)
messages = builder.build_messages(**full_kwargs(tmp_path))
assert [m["role"] for m in messages] == ["system", "user", "assistant", "user"]
assert messages[1]["content"] == "上一轮问题"
assert messages[2]["content"] == "上一轮回答"
user_content = messages[-1]["content"]
assert "帮我总结本周工作进展" in user_content
assert "/tmp/ctx-attachment-notes.txt" in user_content
# system 中不应出现工作空间附件路径提示
assert "ctx-attachment-notes.txt" not in messages[0]["content"]
def test_empty_current_message_falls_back_to_dynamic_context_only(tmp_path):
builder = make_builder(tmp_path)
messages = builder.build_messages(
history=[], current_message="", persona_config=None
)
user_content = messages[-1]["content"]
assert user_content.startswith("## 当前会话上下文")
assert "当前时间" in user_content