mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-09 13:50:00 +08:00
✨ feat!(llm): 重构并升级大语言模型服务为全新 AI 智能体框架 (#2146)
* ✨ feat!(llm): 重构并升级大语言模型服务为全新 AI 智能体框架 - 【重构】将原 services/llm 重构并迁移至全新的 services/ai 架构,提供向下兼容垫片 - 【新增】引入 Agent、Team、Workflow 三大智能体与工作流编排范式 - 【新增】引入基于 RAG 的长期向量记忆与中期槽位记忆系统 - 【新增】引入基于 Docker 的安全代码执行沙箱环境 - 【新增】支持 MCP 协议,允许动态管理和调用 MCP 服务 - 【新增】引入输入输出安全合规护栏与自愈反思机制 - 【优化】重构并优化多厂商 API 适配器 (Gemini, OpenAI, DeepSeek, GLM 等) - 【优化】优化日志脱敏与 Token 预估机制 - 【移除】移除旧版 llm default 和 llm reset-key 命令,新增 llm mcp 管理命令 * 🔧 chore(deps): 更新项目依赖与配置 - 添加 mcp、jieba 和 aiodocker 依赖到配置文件及 requirements.txt - 在 pyright 配置中设置 reportMissingImports 为 none - 调整 .gitignore 中 resources 目录的忽略规则 * ♻️ refactor(tools): 重构工具终止机制并清理知识库日志输出 - 统一使用 `context.state["__end_run__"]` 替代 `EndRunResult` 控制任务结束 - 移除文件系统和向量知识库检索工具中 `ToolResult` 的 `.with_log` 调用 - 调整指令处理器(Directive)的返回值为 `tool_res.output` - 修复部分类型检查警告并优化联合类型判断语法 * ♻️ refactor(tools): 重构工具副作用指令与控制流熔断机制 - 引入 `DirectivePayload` 及 `ToolResult` 的子类以结构化表达工具副作用 - 移除通过 `context.state` 传递魔术变量的隐式控制流设计 - 重构 `DirectiveManager` 处理器接口,直接在处理器中修改 `AgentState` 并构建 `AgentRunResult` - 在 `StandardAgentExecutor` 中统一通过 `directive_manager` 调度工具返回的副作用指令 - 补全 `MessageBuilder` 中部分核心方法的文档注释 * 🐛 fix(sandbox): 修复 Docker 沙箱容器状态检测与会话清理逻辑 -【修复】修正 `is_alive` 中直接读取私有属性的问题,改用 `show()` 返回值 -【修复】解决 `execute_code` 中缓存的执行器与当前会话不一致的问题 -【优化】在清理工作区前增加容器存活检测,避免向已死容器发送请求 -【优化】创建容器时增加运行状态校验,若已停止则自动从缓存中移除并重建 -【优化】优化容器销毁和清理逻辑,静默处理容器不存在 (404) 的异常 * 📝 docs(core): 补充核心模块初始化方法的文档注释 * 🚨 auto fix by pre-commit hooks --------- Co-authored-by: webjoin111 <455457521@qq.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
webjoin111
pre-commit-ci[bot]
parent
bdc1374848
commit
80fc5b86a7
@@ -0,0 +1,688 @@
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from zhenxun.services.ai.context.memory.storage.interfaces import (
|
||||
BaseMemoryReducer,
|
||||
)
|
||||
from zhenxun.services.ai.core.engine.token_counter import token_counter
|
||||
from zhenxun.services.ai.core.messages import (
|
||||
AudioPart,
|
||||
FilePart,
|
||||
ImagePart,
|
||||
LLMMessage,
|
||||
SystemMessage,
|
||||
TextPart,
|
||||
VideoPart,
|
||||
)
|
||||
from zhenxun.services.ai.llm.manager import get_default_model
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.pydantic_compat import model_copy
|
||||
|
||||
|
||||
class MultimodalPlaceholderReducer(BaseMemoryReducer):
|
||||
"""视觉媒体降级:将超过一定轮数的老图片/视频替换为 <图片> 占位符文本"""
|
||||
|
||||
def __init__(self, window_size: int = 5):
|
||||
"""
|
||||
初始化多模态占位符修剪器。
|
||||
|
||||
参数:
|
||||
window_size: 多模态视窗大小。在保留最近的指定数量的多模态消息对后,
|
||||
超出的旧消息中多模态内容(如图片、视频等)将被替换为占位文本。
|
||||
"""
|
||||
self.window_size = window_size
|
||||
|
||||
@staticmethod
|
||||
def apply_multimodal_placeholder(message: LLMMessage) -> LLMMessage:
|
||||
sanitized_message = model_copy(message, deep=False)
|
||||
new_content_parts = []
|
||||
|
||||
for part in sanitized_message.content:
|
||||
if isinstance(part, ImagePart):
|
||||
new_content_parts.append(TextPart(text="<图片>"))
|
||||
elif isinstance(part, AudioPart):
|
||||
new_content_parts.append(TextPart(text="<音频>"))
|
||||
elif isinstance(part, VideoPart):
|
||||
new_content_parts.append(TextPart(text="<视频>"))
|
||||
elif isinstance(part, FilePart):
|
||||
new_content_parts.append(TextPart(text="<文件>"))
|
||||
elif isinstance(part, TextPart) and "[多模态内容:" in part.text:
|
||||
new_content_parts.append(TextPart(text="<图片>"))
|
||||
else:
|
||||
new_content_parts.append(part)
|
||||
|
||||
merged_parts = []
|
||||
for part in new_content_parts:
|
||||
if (
|
||||
isinstance(part, TextPart)
|
||||
and merged_parts
|
||||
and isinstance(merged_parts[-1], TextPart)
|
||||
):
|
||||
new_text = (merged_parts[-1].text or "") + " " + (part.text or "")
|
||||
merged_parts[-1] = TextPart(text=new_text.strip())
|
||||
else:
|
||||
merged_parts.append(part)
|
||||
|
||||
sanitized_message.content = merged_parts
|
||||
sanitized_message.token_cost = None
|
||||
return sanitized_message
|
||||
|
||||
async def reduce(self, messages, current_tokens, model_name, base_overhead=0):
|
||||
if self.window_size <= 0:
|
||||
return messages, False, current_tokens
|
||||
|
||||
processed_messages = []
|
||||
user_multimodal_count = 0
|
||||
changed = False
|
||||
|
||||
for msg in reversed(messages):
|
||||
has_multimodal = False
|
||||
if isinstance(msg.content, list):
|
||||
has_multimodal = any(
|
||||
isinstance(p, ImagePart | AudioPart | VideoPart | FilePart)
|
||||
or (isinstance(p, TextPart) and "[多模态内容:" in p.text)
|
||||
for p in msg.content
|
||||
)
|
||||
|
||||
if has_multimodal:
|
||||
if msg.role == "user":
|
||||
user_multimodal_count += 1
|
||||
if user_multimodal_count > self.window_size:
|
||||
processed_messages.append(self.apply_multimodal_placeholder(msg))
|
||||
changed = True
|
||||
else:
|
||||
processed_messages.append(msg)
|
||||
else:
|
||||
processed_messages.append(msg)
|
||||
|
||||
if not changed:
|
||||
return messages, False, current_tokens
|
||||
|
||||
processed_messages.reverse()
|
||||
new_tokens = token_counter.count_context(
|
||||
processed_messages, model_name, base_overhead
|
||||
)
|
||||
return processed_messages, True, new_tokens
|
||||
|
||||
|
||||
class MessageDropper(BaseMemoryReducer):
|
||||
"""消息丢弃器:在 Token 超过阈值时丢弃最早的非置顶消息对。"""
|
||||
|
||||
def __init__(self, trigger_tokens: int = 4000):
|
||||
"""
|
||||
初始化消息丢弃器。
|
||||
|
||||
参数:
|
||||
trigger_tokens: 触发丢弃策略的 Token 阈值上限。
|
||||
当当前对话 Token 总数超过此值时,将触发硬截断。
|
||||
"""
|
||||
self.trigger_tokens = trigger_tokens
|
||||
|
||||
async def reduce(self, messages, current_tokens, model_name, base_overhead=0):
|
||||
if current_tokens <= self.trigger_tokens:
|
||||
return messages, False, current_tokens
|
||||
|
||||
logger.info(
|
||||
"✂️ [MemoryCompression] 触发硬截断丢弃策略 | 原因: "
|
||||
f"当前 Token 预估 ({current_tokens}) 仍超过硬性上限 ({self.trigger_tokens})," # noqa: E501
|
||||
"开始丢弃最旧的历史对话..."
|
||||
)
|
||||
new_messages = list(messages)
|
||||
changed = False
|
||||
|
||||
while current_tokens > self.trigger_tokens:
|
||||
user_indices = [
|
||||
i
|
||||
for i, m in enumerate(new_messages)
|
||||
if m.role == "user"
|
||||
and not (m.metadata and m.metadata.get("pinned", False))
|
||||
]
|
||||
|
||||
if len(user_indices) < 2:
|
||||
break
|
||||
|
||||
start_idx = user_indices[0]
|
||||
end_idx = user_indices[1]
|
||||
|
||||
del new_messages[start_idx:end_idx]
|
||||
changed = True
|
||||
|
||||
current_tokens = token_counter.count_context(
|
||||
new_messages, model_name, base_overhead
|
||||
)
|
||||
return new_messages, changed, current_tokens
|
||||
|
||||
|
||||
class ToolPrunerReducer(BaseMemoryReducer):
|
||||
"""工具结果修剪器:纯粹计算工具输出的 Token 和轮数,超标时剔除老旧工具返回结果"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
keep_recent_turns: int = 3,
|
||||
trigger_tokens: int = 4000,
|
||||
max_turns: int = 0,
|
||||
):
|
||||
"""
|
||||
初始化工具结果修剪器。
|
||||
|
||||
参数:
|
||||
keep_recent_turns: 保留最近的工具调用轮数(不进行内容截断的轮数)。
|
||||
trigger_tokens: 触发工具修剪策略的工具总 Token 阈值上限。
|
||||
max_turns: 触发工具修剪的最大工具调用轮数上限。若为 0,则不限制轮数。
|
||||
"""
|
||||
self.keep_recent_turns = keep_recent_turns
|
||||
self.trigger_tokens = trigger_tokens
|
||||
self.max_turns = max_turns
|
||||
|
||||
async def reduce(self, messages, current_tokens, model_name, base_overhead=0):
|
||||
tool_msgs = [m for m in messages if m.role == "tool"]
|
||||
tool_turns = len(tool_msgs)
|
||||
|
||||
if tool_turns == 0:
|
||||
return messages, False, current_tokens
|
||||
|
||||
tool_tokens = sum(token_counter.count_message(m, model_name) for m in tool_msgs)
|
||||
|
||||
is_token_exceeded = tool_tokens > self.trigger_tokens
|
||||
is_turn_exceeded = self.max_turns > 0 and tool_turns > self.max_turns
|
||||
|
||||
if not (is_token_exceeded or is_turn_exceeded):
|
||||
return messages, False, current_tokens
|
||||
|
||||
reasons = []
|
||||
if is_token_exceeded:
|
||||
reasons.append(f"工具Token超标 ({tool_tokens} > {self.trigger_tokens})")
|
||||
if is_turn_exceeded:
|
||||
reasons.append(f"工具调用轮数超限 ({tool_turns} > {self.max_turns})")
|
||||
|
||||
logger.info(
|
||||
f"✂️ [MemoryCompression] 触发工具结果修剪策略 | 原因: {' 且 '.join(reasons)}"
|
||||
)
|
||||
|
||||
from zhenxun.services.ai.core.messages import ToolReturnPart
|
||||
|
||||
new_messages = []
|
||||
tools_kept = 0
|
||||
changed = False
|
||||
|
||||
for msg in reversed(messages):
|
||||
if msg.role != "tool":
|
||||
new_messages.append(msg)
|
||||
continue
|
||||
|
||||
if tools_kept < self.keep_recent_turns:
|
||||
tools_kept += 1
|
||||
new_messages.append(msg)
|
||||
continue
|
||||
|
||||
new_content = []
|
||||
part_changed = False
|
||||
for p in msg.content:
|
||||
if isinstance(p, ToolReturnPart):
|
||||
old_len = len(str(p.output))
|
||||
new_p = model_copy(
|
||||
p,
|
||||
update={
|
||||
"output": f"[数据过载自动截断 - 原长度: {old_len} 字符]"
|
||||
},
|
||||
)
|
||||
new_content.append(new_p)
|
||||
part_changed = True
|
||||
changed = True
|
||||
else:
|
||||
new_content.append(p)
|
||||
|
||||
if part_changed:
|
||||
new_msg = model_copy(
|
||||
msg, update={"content": new_content, "token_cost": None}
|
||||
)
|
||||
new_messages.append(new_msg)
|
||||
else:
|
||||
new_messages.append(msg)
|
||||
|
||||
if not changed:
|
||||
return messages, False, current_tokens
|
||||
|
||||
new_messages.reverse()
|
||||
new_total = token_counter.count_context(new_messages, model_name, base_overhead)
|
||||
return new_messages, True, new_total
|
||||
|
||||
|
||||
class AbstractSummarizerReducer(BaseMemoryReducer):
|
||||
"""抽象总结压缩器:提取阈值判断与上下文分流的公共逻辑"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
strategy_name: str,
|
||||
keep_recent_turns: int = 0,
|
||||
trigger_tokens: int = 4000,
|
||||
max_turns: int | None = None,
|
||||
summarization_model: str | None = None,
|
||||
):
|
||||
"""
|
||||
初始化抽象总结压缩基类。
|
||||
|
||||
参数:
|
||||
strategy_name: 压缩策略的名称,用于日志输出和追踪。
|
||||
keep_recent_turns: 压缩时需要保留的最新的对话轮数(不参与总结的轮数)。
|
||||
trigger_tokens: 触发总结策略的 Token 阈值上限。
|
||||
max_turns: 触发总结策略的最大对话轮数上限。
|
||||
summarization_model: 用于执行总结压缩大模型请求的模型名称,若为 None 则使用默认模型。
|
||||
""" # noqa: E501
|
||||
self.strategy_name = strategy_name
|
||||
self.keep_recent_turns = keep_recent_turns
|
||||
self.trigger_tokens = trigger_tokens
|
||||
self.max_turns = max_turns
|
||||
self.summarization_model = summarization_model
|
||||
|
||||
@abstractmethod
|
||||
async def _execute_summarization(
|
||||
self, to_summarize: list[LLMMessage], prev_summary: str
|
||||
) -> LLMMessage | None:
|
||||
"""由子类实现具体的 LLM 调用逻辑,返回新的总结消息"""
|
||||
pass
|
||||
|
||||
async def reduce(self, messages, current_tokens, model_name, base_overhead=0):
|
||||
user_turns = sum(
|
||||
1
|
||||
for m in messages
|
||||
if m.role == "user"
|
||||
and not (m.metadata and m.metadata.get("is_summary", False))
|
||||
)
|
||||
is_token_exceeded = current_tokens > self.trigger_tokens
|
||||
is_turn_exceeded = (
|
||||
self.max_turns is not None
|
||||
and self.max_turns > 0
|
||||
and user_turns > self.max_turns
|
||||
)
|
||||
|
||||
if not (is_token_exceeded or is_turn_exceeded):
|
||||
return messages, False, current_tokens
|
||||
|
||||
reasons = []
|
||||
if is_token_exceeded:
|
||||
reasons.append(f"Token 预估超限 ({current_tokens} > {self.trigger_tokens})")
|
||||
if is_turn_exceeded:
|
||||
reasons.append(f"有效对话轮次超限 ({user_turns} > {self.max_turns})")
|
||||
logger.info(
|
||||
f"🔄 [MemoryCompression] 触发{self.strategy_name}策略 | 原因: "
|
||||
f"{' 且 '.join(reasons)}"
|
||||
)
|
||||
|
||||
pinned_msgs, working_msgs, prev_summary = [], [], ""
|
||||
for msg in messages:
|
||||
is_pinned = isinstance(msg, SystemMessage) or (
|
||||
msg.metadata and msg.metadata.get("pinned", False)
|
||||
)
|
||||
if msg.metadata and msg.metadata.get("is_summary", False):
|
||||
prev_summary = msg.extract_text
|
||||
elif is_pinned:
|
||||
pinned_msgs.append(msg)
|
||||
else:
|
||||
working_msgs.append(msg)
|
||||
|
||||
user_indices = [i for i, m in enumerate(working_msgs) if m.role == "user"]
|
||||
|
||||
if len(user_indices) <= self.keep_recent_turns:
|
||||
return messages, False, current_tokens
|
||||
|
||||
split_idx = (
|
||||
user_indices[-self.keep_recent_turns]
|
||||
if self.keep_recent_turns > 0
|
||||
else len(working_msgs)
|
||||
)
|
||||
to_summarize = working_msgs[:split_idx]
|
||||
to_keep = working_msgs[split_idx:]
|
||||
|
||||
new_summary_msg = await self._execute_summarization(to_summarize, prev_summary)
|
||||
if not new_summary_msg:
|
||||
return messages, False, current_tokens
|
||||
|
||||
new_messages = [*pinned_msgs, new_summary_msg, *to_keep]
|
||||
return (
|
||||
new_messages,
|
||||
True,
|
||||
token_counter.count_context(new_messages, model_name, base_overhead),
|
||||
)
|
||||
|
||||
|
||||
class LLMSummarizerReducer(AbstractSummarizerReducer):
|
||||
"""大模型总结压缩器:将较早的历史对话记录通过 LLM 压缩合并为一段文本摘要。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
keep_recent_turns: int = 0,
|
||||
trigger_tokens: int = 4000,
|
||||
max_turns: int | None = None,
|
||||
summarization_model: str | None = None,
|
||||
summarization_prompt: str = (
|
||||
"请概括以下对话内容,保留关键的约束条件、用户偏好、"
|
||||
"已完成的任务状态和未解决的问题。"
|
||||
),
|
||||
):
|
||||
"""
|
||||
初始化大模型总结压缩器。
|
||||
|
||||
参数:
|
||||
keep_recent_turns: 压缩时需要保留的最新的对话轮数。
|
||||
trigger_tokens: 触发总结策略的 Token 阈值上限。
|
||||
max_turns: 触发总结策略的最大对话轮数上限。
|
||||
summarization_model: 用于执行总结压缩的大模型名称。
|
||||
summarization_prompt: 发送给大模型的总结引导 Prompt 提示词。
|
||||
"""
|
||||
super().__init__(
|
||||
strategy_name="历史对话合并总结",
|
||||
keep_recent_turns=keep_recent_turns,
|
||||
trigger_tokens=trigger_tokens,
|
||||
max_turns=max_turns,
|
||||
summarization_model=summarization_model,
|
||||
)
|
||||
self.summarization_prompt = summarization_prompt
|
||||
|
||||
async def _execute_summarization(
|
||||
self, to_summarize: list[LLMMessage], prev_summary: str
|
||||
) -> LLMMessage | None:
|
||||
prompt_text = f"### 📋 [对话摘要任务]\n{self.summarization_prompt}\n\n"
|
||||
if prev_summary:
|
||||
prompt_text += "#### önceki_summary (参考先前的快照):\n"
|
||||
prompt_text += f"> {prev_summary}\n\n"
|
||||
prompt_text += "#### 待处理的历史消息流:\n"
|
||||
for m in to_summarize:
|
||||
c_str = m.extract_text[:1500]
|
||||
speaker = m.source_name if m.source_name else m.role.capitalize()
|
||||
prompt_text += f"[{speaker}]: {c_str}\n"
|
||||
prompt_text += "</需要合并的旧对话记录>\n"
|
||||
|
||||
from zhenxun.services.ai.llm.api import chat
|
||||
|
||||
try:
|
||||
model_to_use = self.summarization_model or get_default_model("chat")
|
||||
response = await chat(
|
||||
prompt_text,
|
||||
model=model_to_use,
|
||||
instruction="你是后台记忆整理引擎。请客观、简明输出当前对话全局摘要。",
|
||||
)
|
||||
new_summary_msg = LLMMessage.assistant_text_response(
|
||||
f"【历史对话摘要记忆】\n{response.text}"
|
||||
)
|
||||
new_summary_msg.metadata = {"is_summary": True, "pinned": True}
|
||||
return new_summary_msg
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[{self.__class__.__name__}] 压缩总结调用失败,已跳过本次压缩: {e}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
_T_Summary = TypeVar("_T_Summary", bound=BaseModel)
|
||||
|
||||
|
||||
class StructuredSummaryReducer(AbstractSummarizerReducer, Generic[_T_Summary]):
|
||||
"""结构化总结压缩器:基于 JSON Schema 格式化抽取长上下文状态信息并合并"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
response_model: type[_T_Summary],
|
||||
prompt_template: str,
|
||||
format_callback: Callable[[_T_Summary], str],
|
||||
keep_recent_turns: int = 0,
|
||||
trigger_tokens: int = 4000,
|
||||
max_turns: int | None = None,
|
||||
summarization_model: str | None = None,
|
||||
instruction: str = (
|
||||
"请提取并合并先前的状态和最新的对话内容,保持精简,不要编造事实"
|
||||
),
|
||||
):
|
||||
"""
|
||||
初始化结构化总结压缩器。
|
||||
|
||||
参数:
|
||||
response_model: 接收结构化输出的 Pydantic 模型类,需继承自 BaseModel。
|
||||
prompt_template: 用于抽取合并状态的 Prompt 模板,包含 {prev_summary} 和 {dialogue} 占位符。
|
||||
format_callback: 格式化回调函数,用于将结构化 Pydantic 响应对象转换为便于大模型阅读的字符串。
|
||||
keep_recent_turns: 压缩时需要保留的最新的对话轮数。
|
||||
trigger_tokens: 触发总结策略的 Token 阈值上限。
|
||||
max_turns: 触发总结策略的最大对话轮数上限。
|
||||
summarization_model: 用于执行总结压缩的大模型名称。
|
||||
instruction: 指导大模型生成结构化数据时的系统指令说明。
|
||||
""" # noqa: E501
|
||||
super().__init__(
|
||||
strategy_name="结构化状态抽取压缩",
|
||||
keep_recent_turns=keep_recent_turns,
|
||||
trigger_tokens=trigger_tokens,
|
||||
max_turns=max_turns,
|
||||
summarization_model=summarization_model,
|
||||
)
|
||||
self.response_model = response_model
|
||||
self.prompt_template = prompt_template
|
||||
self.format_callback = format_callback
|
||||
self.instruction = instruction
|
||||
|
||||
async def _execute_summarization(
|
||||
self, to_summarize: list[LLMMessage], prev_summary: str
|
||||
) -> LLMMessage | None:
|
||||
dialogue_text = ""
|
||||
for m in to_summarize:
|
||||
c_str = m.extract_text[:1500]
|
||||
speaker = m.source_name if m.source_name else m.role.capitalize()
|
||||
dialogue_text += f"[{speaker}]: {c_str}\n"
|
||||
|
||||
prompt_text = self.prompt_template.format(
|
||||
prev_summary=prev_summary, dialogue=dialogue_text
|
||||
)
|
||||
|
||||
from zhenxun.services.ai.llm.api import generate_structured
|
||||
|
||||
try:
|
||||
model_to_use = self.summarization_model or get_default_model("chat")
|
||||
summary_obj = await generate_structured(
|
||||
prompt_text,
|
||||
response_model=self.response_model,
|
||||
model=model_to_use,
|
||||
instruction=self.instruction,
|
||||
)
|
||||
|
||||
summary_text = self.format_callback(summary_obj)
|
||||
|
||||
new_summary_msg = LLMMessage.assistant_text_response(
|
||||
f"【历史状态摘要记忆】\n{summary_text}"
|
||||
)
|
||||
new_summary_msg.metadata = {"is_summary": True, "pinned": True}
|
||||
return new_summary_msg
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[{self.__class__.__name__}] 结构化总结失败,已跳过本次压缩: {e}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class CondenserPipeline:
|
||||
"""上下文压缩流水线:按顺序依次执行各阶段的记忆压缩减项。"""
|
||||
|
||||
def __init__(self, reducers: list[BaseMemoryReducer]):
|
||||
"""
|
||||
初始化上下文压缩流水线。
|
||||
|
||||
参数:
|
||||
reducers: 压缩减项器列表,将按顺序对记忆进行多阶段修剪和压缩。
|
||||
"""
|
||||
self.reducers = reducers
|
||||
|
||||
@classmethod
|
||||
def create_from_configs(
|
||||
cls, memory_config: Any, model_name: str
|
||||
) -> "CondenserPipeline":
|
||||
"""基于全局和局部配置组装压缩管线工厂方法"""
|
||||
from zhenxun.services.ai.config import get_llm_config
|
||||
from zhenxun.services.ai.llm.system.capabilities import get_model_capabilities
|
||||
|
||||
config = get_llm_config().context_settings
|
||||
pipeline_reducers = []
|
||||
caps = get_model_capabilities(model_name)
|
||||
|
||||
vw = config.vision_window_size
|
||||
if memory_config and memory_config.compression.vision_window is not None:
|
||||
vw = memory_config.compression.vision_window
|
||||
if vw > 0:
|
||||
pipeline_reducers.append(MultimodalPlaceholderReducer(window_size=vw))
|
||||
|
||||
tp = config.tool_pruning
|
||||
if tp.enable:
|
||||
tp_limit = (
|
||||
int(caps.max_input_tokens * tp.trigger_threshold)
|
||||
if tp.trigger_threshold <= 1.0
|
||||
else int(tp.trigger_threshold)
|
||||
)
|
||||
pipeline_reducers.append(
|
||||
ToolPrunerReducer(
|
||||
keep_recent_turns=tp.keep_recent_turns,
|
||||
trigger_tokens=tp_limit,
|
||||
max_turns=tp.max_history_turns,
|
||||
)
|
||||
)
|
||||
|
||||
policy = memory_config.compression.policy if memory_config else None
|
||||
if policy is not None:
|
||||
pipeline_reducers.extend(policy)
|
||||
else:
|
||||
threshold = config.llm_summary.trigger_threshold
|
||||
if memory_config and memory_config.compression.threshold is not None:
|
||||
threshold = memory_config.compression.threshold
|
||||
|
||||
limit = (
|
||||
int(caps.max_input_tokens * threshold)
|
||||
if threshold <= 1.0
|
||||
else int(threshold)
|
||||
)
|
||||
|
||||
max_turns = config.llm_summary.max_history_turns
|
||||
if (
|
||||
memory_config
|
||||
and memory_config.compression.max_history_turns is not None
|
||||
):
|
||||
max_turns = memory_config.compression.max_history_turns
|
||||
|
||||
if config.llm_summary.enable:
|
||||
pipeline_reducers.extend(
|
||||
MemoryPolicy.llm_summarize(
|
||||
trigger_tokens=limit,
|
||||
max_turns=max_turns,
|
||||
keep_recent_turns=config.llm_summary.keep_recent_turns,
|
||||
summarization_model=config.llm_summary.summarization_model,
|
||||
summarization_prompt=config.llm_summary.summarization_prompt,
|
||||
)
|
||||
)
|
||||
else:
|
||||
pipeline_reducers.extend(MemoryPolicy.unlimited())
|
||||
|
||||
return cls(pipeline_reducers)
|
||||
|
||||
async def run(
|
||||
self, messages, model_name, base_overhead=0
|
||||
) -> tuple[list[LLMMessage], bool]:
|
||||
current_tokens = token_counter.count_context(
|
||||
messages, model_name, base_overhead
|
||||
)
|
||||
|
||||
current_messages = messages
|
||||
any_changed = False
|
||||
for reducer in self.reducers:
|
||||
current_messages, changed, current_tokens = await reducer.reduce(
|
||||
current_messages,
|
||||
current_tokens,
|
||||
model_name,
|
||||
base_overhead,
|
||||
)
|
||||
if changed:
|
||||
any_changed = True
|
||||
return current_messages, any_changed
|
||||
|
||||
|
||||
class MemoryPolicy:
|
||||
"""
|
||||
记忆策略工厂 (Strategy Factory Facade)。
|
||||
为开发者提供开箱即用的上下文压缩管线组装方案。
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def unlimited() -> list[BaseMemoryReducer]:
|
||||
"""无限制模式。不进行任何形式的截断和总结,适用于短对话或纯 Agent 内部流转。"""
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def llm_summarize(
|
||||
trigger_tokens: int = 4000,
|
||||
max_turns: int | None = None,
|
||||
keep_recent_turns: int = 0,
|
||||
summarization_model: str | None = None,
|
||||
summarization_prompt: str = (
|
||||
"请概括以下对话内容,保留关键的约束条件、用户偏好、"
|
||||
"已完成的任务状态和未解决的问题。"
|
||||
),
|
||||
) -> list[BaseMemoryReducer]:
|
||||
"""LLM 总结压缩模式。Token 达标后,自动将历史对话合并为一段 Summary。"""
|
||||
return [
|
||||
LLMSummarizerReducer(
|
||||
keep_recent_turns=keep_recent_turns,
|
||||
trigger_tokens=trigger_tokens,
|
||||
max_turns=max_turns,
|
||||
summarization_model=summarization_model,
|
||||
summarization_prompt=summarization_prompt,
|
||||
),
|
||||
MessageDropper(trigger_tokens=trigger_tokens),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def structured_summarize(
|
||||
trigger_tokens: int = 4000,
|
||||
max_turns: int | None = None,
|
||||
keep_recent_turns: int = 0,
|
||||
summarization_model: str | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
prompt_template: str | None = None,
|
||||
format_callback: Callable[[Any], str] | None = None,
|
||||
) -> list[BaseMemoryReducer]:
|
||||
"""结构化总结压缩模式。使用 JSON Schema 强制大模型提取核心状态。"""
|
||||
|
||||
class DefaultStateSummary(BaseModel):
|
||||
user_context: str = Field(
|
||||
description="用户的核心意图、诉求、人设或长期记忆规则。"
|
||||
)
|
||||
completed_tasks: str = Field(description="已完成的操作或已经确认的情节。")
|
||||
pending_tasks: str = Field(description="正在进行中的任务或尚未解答的问题。")
|
||||
current_state: str = Field(
|
||||
description="当前状态,如重要变量、玩家血量、关键物品坐标等。"
|
||||
)
|
||||
|
||||
def default_format(obj: DefaultStateSummary) -> str:
|
||||
return (
|
||||
f"👤 用户上下文: {obj.user_context}\n"
|
||||
f"✅ 已完成/确认: {obj.completed_tasks}\n"
|
||||
f"⏳ 待处理/疑问: {obj.pending_tasks}\n"
|
||||
f"📌 当前状态: {obj.current_state}"
|
||||
)
|
||||
|
||||
default_prompt = (
|
||||
"你是一个专门用于长上下文状态压缩的引擎。请阅读以下先前的总结和旧对话,"
|
||||
"提取核心状态信息,并合并它们。\n\n"
|
||||
"<之前的状态摘要>\n{prev_summary}\n</之前的状态摘要>\n\n"
|
||||
"<需要合并的旧对话记录>\n"
|
||||
"{dialogue}"
|
||||
"</需要合并的旧对话记录>\n"
|
||||
)
|
||||
|
||||
return [
|
||||
StructuredSummaryReducer(
|
||||
response_model=response_model or DefaultStateSummary,
|
||||
prompt_template=prompt_template or default_prompt,
|
||||
format_callback=format_callback or default_format,
|
||||
keep_recent_turns=keep_recent_turns,
|
||||
trigger_tokens=trigger_tokens,
|
||||
max_turns=max_turns,
|
||||
summarization_model=summarization_model,
|
||||
),
|
||||
MessageDropper(trigger_tokens=trigger_tokens),
|
||||
]
|
||||
Reference in New Issue
Block a user