更新压缩机制,提高前缀缓存命中率,优化UI渲染机制,提高帧数
This commit is contained in:
1 parent
6131c2563e
commit
0ce72f74f1
175 files changed
+7621
-2611
No files matched your search
@@ -0,0 +1,254 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""链式压缩块重构验证:原生历史重建 / 压缩节点 / 双次压缩块链 / payload 透传。
|
||||
|
||||
运行(Agent 目录下): python tests/verify_compression_chain.py
|
||||
覆盖:
|
||||
T0 单元渲染/合并/token 估算
|
||||
T1 _rebuild_turn_messages 原生重建顺序(并行合并/尾部去重/最终回复)
|
||||
T2 build_native_history_messages 存储路径 + 游标跳过
|
||||
T3 compressed_context 读写往返
|
||||
T4 maybe_compress_context 单次压缩(范围/统计/RemoveMessage/块链)
|
||||
T5 双次压缩(seq 递增、旧块保留、游标推进)
|
||||
T6 llm payload reasoning_content 透传
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "src"))
|
||||
|
||||
import agent.utils.agent_utils as au
|
||||
from agent.history import tool_call_recorder as tcr
|
||||
from agent.utils.agent_utils import load_compressed_context, save_compressed_context
|
||||
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
|
||||
|
||||
PASSED = []
|
||||
|
||||
|
||||
def check(name: str, cond: bool, detail: str = "") -> None:
|
||||
if not cond:
|
||||
raise AssertionError(f"FAIL {name}: {detail}")
|
||||
PASSED.append(name)
|
||||
print(f" ok {name}")
|
||||
|
||||
|
||||
def section(t: str) -> None:
|
||||
print(f"\n== {t} ==")
|
||||
|
||||
|
||||
# ---------- 测试目录 ----------
|
||||
tmp_root = tempfile.mkdtemp(prefix="verify_compress_")
|
||||
SHARE_SID = "20990101_000000"
|
||||
try:
|
||||
au.SESSIONS_ROOT = os.path.join(tmp_root, "sessions")
|
||||
os.makedirs(au.SESSIONS_ROOT, exist_ok=True)
|
||||
sess_dir = os.path.join(au.SESSIONS_ROOT, SHARE_SID)
|
||||
os.makedirs(sess_dir, exist_ok=True)
|
||||
# 测试图片/文件(供 _native_user_message 绝对路径解析)
|
||||
Path(os.path.join(au.SESSIONS_ROOT, "20990101_120001")).mkdir(exist_ok=True)
|
||||
(Path(au.SESSIONS_ROOT) / "20990101_120001" / "in.png").write_bytes(b"\x89PNG\r\n")
|
||||
(Path(au.SESSIONS_ROOT) / "20990101_120001" / "data.pdf").write_bytes(b"1 0 R")
|
||||
|
||||
# ---------- T0 ----------
|
||||
section("T0 单元渲染/合并/token 估算")
|
||||
from agent.core import nodes
|
||||
check("ascii tokens", nodes._estimate_text_tokens("abcd") == 1, str(nodes._estimate_text_tokens("abcd")))
|
||||
check("cjk tokens", nodes._estimate_text_tokens("中文文本") == 4)
|
||||
merged = nodes._merge_units([
|
||||
HumanMessage(content="m1"),
|
||||
HumanMessage(content="m2"),
|
||||
AIMessage(content="a1"),
|
||||
AIMessage(content="a2", additional_kwargs={"reasoning_content": "r"},
|
||||
tool_calls=[{"id": "t1", "name": "t", "args": {}}]),
|
||||
ToolMessage(content="tool ok", tool_call_id="t1", name="t"),
|
||||
])
|
||||
check("合并单元数=3(human 合并 1 + ai 合并 1 + tool 1)", len(merged) == 3, str(len(merged)))
|
||||
check("human 合并含两条文本", "m1" in merged[0] and "m2" in merged[0])
|
||||
check("ai 合并含思考与调用", "r" in merged[1] and "工具调用" in merged[1])
|
||||
|
||||
# ---------- T1 ----------
|
||||
section("T1 _rebuild_turn_messages 原生重建顺序/id")
|
||||
TID1 = "20990101_120001"
|
||||
turn_msgs = [
|
||||
{"role": "user", "user": {"content": [
|
||||
{"type": "text", "text": "帮我处理图片"},
|
||||
{"type": "image_url", "relpath": f"{TID1}/in.png", "name": "in.png"},
|
||||
{"type": "file", "relpath": f"{TID1}/data.pdf", "name": "data.pdf"},
|
||||
]}},
|
||||
{"role": "assistant", "assistant": {"text": "处理完成"}},
|
||||
]
|
||||
calls = [
|
||||
{"id": "c1", "name": "read_image", "args": {"path": "in.png"}, "status": "done",
|
||||
"thinking": "思考A", "reflection": "反思A", "result_text": "图片内容是文本",
|
||||
"group_index": 0, "result_images": [], "result_files": []},
|
||||
{"id": "c2", "name": "read_file", "args": {"path": "data.pdf"}, "status": "done",
|
||||
"thinking": "", "reflection": "", "result_text": "PDF 正文",
|
||||
"group_index": 0, "result_images": [], "result_files": ["out.txt"]},
|
||||
{"id": "c3", "name": "call_subagent", "args": {"task": "x"}, "status": "done",
|
||||
"thinking": "", "reflection": "", "result_text": "子任务完成",
|
||||
"group_index": 1, "result_images": [], "result_files": []},
|
||||
]
|
||||
tail = [
|
||||
{"thought": "思考A", "reflection": "反思A"}, # 与组内重复 → 去重
|
||||
{"thought": "尾部思考", "reflection": ""}, # 未消费 → 注入
|
||||
]
|
||||
msgs = au._rebuild_turn_messages(TID1, turn_msgs, calls, tail)
|
||||
kinds = [type(m).__name__ for m in msgs]
|
||||
expect_kinds = ["HumanMessage", "AIMessage", "ToolMessage", "ToolMessage",
|
||||
"AIMessage", "ToolMessage", "AIMessage", "AIMessage"]
|
||||
check("顺序 user->AI(c1c2)->Tool2->AI(c3)->Tool1->tail->final",
|
||||
kinds == expect_kinds, str(kinds))
|
||||
check("user 多模态还原(text+image_url+file)", len(msgs[0].content) == 3,
|
||||
str(msgs[0].content))
|
||||
img_part = msgs[0].content[1]
|
||||
check("image part 绝对路径", img_part["type"] == "image_url" and
|
||||
str(img_part["image_url"]["url"]).endswith(os.path.join(TID1, "in.png")),
|
||||
str(img_part))
|
||||
file_part = msgs[0].content[2]
|
||||
check("file part 绝对路径", file_part["type"] == "file" and
|
||||
str(file_part["path"]).endswith(os.path.join(TID1, "data.pdf")), str(file_part))
|
||||
a0 = msgs[1]
|
||||
check("AIMessage0 合并两条并行调用", len(a0.tool_calls) == 2, str(a0.tool_calls))
|
||||
check("AIMessage0 反思→content", a0.content == "反思A", repr(a0.content))
|
||||
check("AIMessage0 思考→reasoning_content",
|
||||
(a0.additional_kwargs or {}).get("reasoning_content") == "思考A",
|
||||
str(a0.additional_kwargs))
|
||||
check("AIMessage0 id", a0.id == f"hist_assist_{TID1}_0", str(a0.id))
|
||||
check("Tool c1 匹配 id/name", msgs[2].tool_call_id == "c1" and msgs[2].name == "read_image")
|
||||
check("Tool c2 含工具产物提示", "out.txt" in msgs[3].content, msgs[3].content)
|
||||
check("AIMessage1 单调用", len(msgs[4].tool_calls) == 1 and msgs[4].id == f"hist_assist_{TID1}_1")
|
||||
check("Tail 去重+注入", msgs[6].id == f"hist_tail_{TID1}_0" and
|
||||
(msgs[6].additional_kwargs or {}).get("reasoning_content") == "尾部思考",
|
||||
str(msgs[6].id))
|
||||
check("Final id/内容", msgs[7].id == f"hist_final_{TID1}" and msgs[7].content == "处理完成")
|
||||
check("全部 synthetic/tool_ctx 标记", all((m.additional_kwargs or {}).get("tool_ctx") for m in msgs))
|
||||
|
||||
# ---------- T2 ----------
|
||||
section("T2 build_native_history_messages 存储路径 + 游标跳过")
|
||||
turn_path = Path(sess_dir) / f"turn_{TID1}.json"
|
||||
turn_path.write_text(json.dumps({"messages": turn_msgs}, ensure_ascii=False), encoding="utf-8")
|
||||
tool_path = Path(sess_dir) / f"tool_{TID1}.json"
|
||||
tool_path.write_text(json.dumps({
|
||||
"turn_id": TID1,
|
||||
"tool_calls": calls,
|
||||
"reflections": tail,
|
||||
}, ensure_ascii=False), encoding="utf-8")
|
||||
hist0 = au.build_native_history_messages(SHARE_SID, 0)
|
||||
check("cursor=0 重建 8 条", len(hist0) == 8, str(len(hist0)))
|
||||
hist4 = au.build_native_history_messages(SHARE_SID, 4)
|
||||
check("cursor=4 跳过前 4 条", len(hist4) == 4 and hist4[0].id == f"hist_assist_{TID1}_1",
|
||||
str([m.id for m in hist4]))
|
||||
check("cursor=8 无剩余", len(au.build_native_history_messages(SHARE_SID, 8)) == 0)
|
||||
|
||||
# ---------- T3 ----------
|
||||
section("T3 compressed_context 读写往返(真实文件)")
|
||||
ctx1 = {"version": 2, "cursor_msg": 0, "blocks": [
|
||||
{"seq": 1, "content": "块1摘要", "msg_from": 1, "msg_to": 8, "units": 8,
|
||||
"merged_units": 7, "turns": 1, "tokens_before": 900, "tokens_after": 60,
|
||||
"compressed_at": 1},
|
||||
]}
|
||||
save_compressed_context(SHARE_SID, ctx1)
|
||||
loaded = load_compressed_context(SHARE_SID)
|
||||
check("往返一致", loaded == ctx1, str(loaded)[:200])
|
||||
check("新键结构", "blocks" in loaded and loaded["version"] == 2, str(loaded)[:120])
|
||||
|
||||
# ---------- T4 ----------
|
||||
section("T4 maybe_compress_context 单次压缩(范围/统计/RemoveMessage/块链)")
|
||||
seen = {"live": [], "result": []}
|
||||
tcr.get_current_session = lambda: SHARE_SID
|
||||
tcr.is_sub_agent_context = lambda: False
|
||||
tcr.record_tool_call_live = lambda sid, name, args: seen["live"].append((sid, name, args))
|
||||
tcr.record_tool_result_live = lambda sid, name, text: seen["result"].append((sid, name, text))
|
||||
au.summarize_chat_text = lambda llm, text: "SUM:" + text[:80]
|
||||
|
||||
state_msgs = [SystemMessage(content="sys")]
|
||||
state_msgs.append(SystemMessage(content="【历史对话摘要 #1】\n块1摘要", id="compressed_block_1"))
|
||||
state_msgs.extend(au.build_native_history_messages(SHARE_SID, 0))
|
||||
state_msgs.append(HumanMessage(content="当前轮用户问题")) # 无 id → 不压缩
|
||||
out1 = nodes.maybe_compress_context({"messages": state_msgs, "_need_compress": True})
|
||||
upd = out1["messages"]
|
||||
rm = [u for u in upd if type(u).__name__ == "RemoveMessage"]
|
||||
new_sys = [u for u in upd if isinstance(u, SystemMessage)]
|
||||
check("RemoveMessage 移除全部 8 条 hist_*", len(rm) == 8, str(len(rm)))
|
||||
check("新增 1 个块 SystemMessage", len(new_sys) == 1)
|
||||
check("新块 id=compressed_block_2", new_sys[0].id == "compressed_block_2", str(new_sys[0].id))
|
||||
check("新块内容带序号前缀", new_sys[0].content.startswith("【历史对话摘要 #2】\nSUM:"),
|
||||
str(new_sys[0].content)[:40])
|
||||
check("_need_compress 复位", out1.get("_need_compress") is False)
|
||||
ctx2 = load_compressed_context(SHARE_SID)
|
||||
check("块链追加(2 块,新块 seq=2)", len(ctx2["blocks"]) == 2 and ctx2["blocks"][1]["seq"] == 2,
|
||||
str(len(ctx2["blocks"])))
|
||||
b2 = ctx2["blocks"][1]
|
||||
check("统计 msg 区间 1-8", b2["msg_from"] == 1 and b2["msg_to"] == 8, str(b2))
|
||||
check("统计 turns=1 / units=8 / merged=7", b2["turns"] == 1 and b2["units"] == 8 and b2["merged_units"] == 7,
|
||||
f"turns={b2['turns']} units={b2['units']} merged={b2['merged_units']}")
|
||||
check("统计 tokens_before>tokens_after>0",
|
||||
b2["tokens_before"] > b2["tokens_after"] > 0,
|
||||
f"{b2.get('tokens_before')}->{b2.get('tokens_after')}")
|
||||
check("游标推进到 8", ctx2["cursor_msg"] == 8, str(ctx2["cursor_msg"]))
|
||||
check("SSE 压缩开始事件", seen["live"] and seen["live"][-1][0] == SHARE_SID and seen["live"][-1][2]["action"] == "上下文压缩中",
|
||||
str(seen["live"])[:120])
|
||||
check("SSE 压缩完成事件", seen["result"] and "压缩完成" in seen["result"][-1][2],
|
||||
str(seen["result"])[:120])
|
||||
|
||||
# ---------- T5 ----------
|
||||
section("T5 双次压缩(seq 递增、旧块保留、游标推进)")
|
||||
TID2 = "21090102_090000"
|
||||
turn2_msgs = [
|
||||
{"role": "user", "user": {"content": "第二轮问题"}},
|
||||
{"role": "assistant", "assistant": {"text": "第二轮答复"}},
|
||||
]
|
||||
(Path(sess_dir) / f"turn_{TID2}.json").write_text(
|
||||
json.dumps({"messages": turn2_msgs}, ensure_ascii=False), encoding="utf-8")
|
||||
calls2 = [
|
||||
{"id": "c4", "name": "search", "args": {"q": "xy"}, "status": "done",
|
||||
"thinking": "思考B", "reflection": "", "result_text": "结果B",
|
||||
"group_index": 0, "result_images": [], "result_files": []},
|
||||
]
|
||||
(Path(sess_dir) / f"tool_{TID2}.json").write_text(
|
||||
json.dumps({"turn_id": TID2, "tool_calls": calls2, "reflections": []},
|
||||
ensure_ascii=False), encoding="utf-8")
|
||||
hist_t2 = au.build_native_history_messages(SHARE_SID, 8)
|
||||
check("游标后新内容 4 条(user+AI+tool+final)", len(hist_t2) == 4, str([m.id for m in hist_t2]))
|
||||
au.summarize_chat_text = lambda llm, text: "SUM2:" + text[:60]
|
||||
state2_msgs = [SystemMessage(content="sys")]
|
||||
for b in load_compressed_context(SHARE_SID)["blocks"]:
|
||||
state2_msgs.append(SystemMessage(content=f"#{b['seq']}:{b['content']}", id=f"compressed_block_{b['seq']}"))
|
||||
state2_msgs.extend(hist_t2)
|
||||
state2_msgs.append(HumanMessage(content="第二轮当前问题"))
|
||||
out2 = nodes.maybe_compress_context({"messages": state2_msgs, "_need_compress": True})
|
||||
upd2 = out2["messages"]
|
||||
rm2 = [u for u in upd2 if type(u).__name__ == "RemoveMessage"]
|
||||
new_sys2 = [u for u in upd2 if isinstance(u, SystemMessage)]
|
||||
check("第二次 RemoveMessage 4 条", len(rm2) == 4, str(len(rm2)))
|
||||
check("第二次新块 seq=3", new_sys2[0].id == "compressed_block_3", str(new_sys2[0].id))
|
||||
ctx3 = load_compressed_context(SHARE_SID)
|
||||
check("块链 3 块且旧块内容未变",
|
||||
len(ctx3["blocks"]) == 3 and ctx3["blocks"][0]["content"] == "块1摘要" and
|
||||
ctx3["blocks"][1]["content"].startswith("SUM:"), str(len(ctx3["blocks"])))
|
||||
b3 = ctx3["blocks"][2]
|
||||
check("第 2 次统计区间 9-12", b3["msg_from"] == 9 and b3["msg_to"] == 12, str(b3))
|
||||
check("游标推进到 12", ctx3["cursor_msg"] == 12, str(ctx3["cursor_msg"]))
|
||||
|
||||
# ---------- T6 ----------
|
||||
section("T6 llm payload reasoning_content 透传")
|
||||
from agent.core.llm import Multimodel_LLM
|
||||
llm = Multimodel_LLM(model="test-model", api_key="sk-test", base_url="http://localhost:9/v1")
|
||||
payload = llm._get_request_payload([
|
||||
HumanMessage(content="用户问题"),
|
||||
AIMessage(content="反思A", additional_kwargs={"reasoning_content": "思考A"},
|
||||
tool_calls=[{"id": "c1", "name": "t", "args": {}}]),
|
||||
ToolMessage(content="结果", tool_call_id="c1", name="t"),
|
||||
])
|
||||
pdicts = payload["messages"]
|
||||
check("payload 3 条消息", len(pdicts) == 3, str(len(pdicts)))
|
||||
asst = [p for p in pdicts if p.get("role") == "assistant"][0]
|
||||
check("reasoning_content 透传", asst.get("reasoning_content") == "思考A", str(asst))
|
||||
check("tool_calls 保留", len(asst.get("tool_calls") or []) == 1)
|
||||
|
||||
print(f"\n全部通过: {len(PASSED)} 项")
|
||||
finally:
|
||||
shutil.rmtree(tmp_root, ignore_errors=True)
|
||||
Reference in new issue
Block a user