mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
refactor: 活动日志摘要改用 LLM 总结替代文本截取
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
活动日志中间件 - 自动记录 Agent 每次交互的操作摘要。
|
活动日志中间件 - 自动记录 Agent 每次交互的操作摘要。
|
||||||
|
|
||||||
按日期存储在 CONFIG_PATH/agent/activity/YYYY-MM-DD.md 中,
|
按日期存储在 CONFIG_PATH/agent/activity/YYYY-MM-DD.md 中,
|
||||||
每次 Agent 执行完毕后自动追加一条活动记录,
|
每次 Agent 执行完毕后自动调用 LLM 对本轮对话生成简洁的活动摘要,
|
||||||
并在每次 Agent 启动时加载近几天的活动日志注入系统提示词。
|
并在每次 Agent 启动时加载近几天的活动日志注入系统提示词。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ from langchain.agents.middleware.types import (
|
|||||||
PrivateStateAttr, # noqa
|
PrivateStateAttr, # noqa
|
||||||
ResponseT,
|
ResponseT,
|
||||||
)
|
)
|
||||||
from langchain_core.messages import AIMessage, HumanMessage
|
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
||||||
from langgraph.runtime import Runtime
|
from langgraph.runtime import Runtime
|
||||||
|
|
||||||
from app.agent.middleware.utils import append_to_system_message
|
from app.agent.middleware.utils import append_to_system_message
|
||||||
@@ -30,15 +30,23 @@ from app.log import logger
|
|||||||
# 活动日志保留天数
|
# 活动日志保留天数
|
||||||
DEFAULT_RETENTION_DAYS = 7
|
DEFAULT_RETENTION_DAYS = 7
|
||||||
|
|
||||||
# 单次活动记录的最大长度
|
|
||||||
MAX_ENTRY_LENGTH = 300
|
|
||||||
|
|
||||||
# 注入系统提示词时加载的天数
|
# 注入系统提示词时加载的天数
|
||||||
PROMPT_LOAD_DAYS = 3
|
PROMPT_LOAD_DAYS = 3
|
||||||
|
|
||||||
# 每日日志文件最大大小 (256KB)
|
# 每日日志文件最大大小 (256KB)
|
||||||
MAX_LOG_FILE_SIZE = 256 * 1024
|
MAX_LOG_FILE_SIZE = 256 * 1024
|
||||||
|
|
||||||
|
# 提取本轮对话上下文的最大字符数(避免过长的对话消耗太多 token)
|
||||||
|
MAX_CONTEXT_FOR_SUMMARY = 4000
|
||||||
|
|
||||||
|
# LLM 总结的提示词
|
||||||
|
SUMMARY_PROMPT = """请根据以下 AI 助手与用户的对话记录,生成一条简洁的活动摘要(中文,一句话,不超过80字)。
|
||||||
|
摘要应包含:用户的需求是什么、助手做了什么、结果如何。
|
||||||
|
只输出摘要内容,不要加任何前缀、标点序号或解释。
|
||||||
|
|
||||||
|
对话记录:
|
||||||
|
{conversation}"""
|
||||||
|
|
||||||
|
|
||||||
class ActivityLogState(AgentState):
|
class ActivityLogState(AgentState):
|
||||||
"""ActivityLogMiddleware 的状态模型。"""
|
"""ActivityLogMiddleware 的状态模型。"""
|
||||||
@@ -53,22 +61,21 @@ class ActivityLogStateUpdate(TypedDict):
|
|||||||
activity_log_contents: dict[str, str]
|
activity_log_contents: dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
def _extract_activity_summary(messages: list) -> str | None:
|
def _extract_last_round(messages: list) -> list | None:
|
||||||
"""从本次对话的消息列表中提取活动摘要。
|
"""从完整消息列表中提取最后一轮交互。
|
||||||
|
|
||||||
只关注最后一轮交互(从最后一条用户消息到结尾),
|
从最后一条 HumanMessage 到消息末尾即为本轮交互。
|
||||||
分析用户问题、Agent 使用的工具、最终回复,生成一行简洁的活动描述。
|
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
messages: Agent 执行后的完整消息列表。
|
messages: Agent 执行后的完整消息列表。
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
活动摘要字符串,如果没有有意义的活动则返回 None。
|
本轮交互的消息子列表,如果无有效交互则返回 None。
|
||||||
"""
|
"""
|
||||||
if not messages:
|
if not messages:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 找到最后一条用户消息的索引,从此处开始截取本轮交互
|
# 找到最后一条用户消息的索引
|
||||||
last_human_idx = None
|
last_human_idx = None
|
||||||
for i in range(len(messages) - 1, -1, -1):
|
for i in range(len(messages) - 1, -1, -1):
|
||||||
if isinstance(messages[i], HumanMessage) and messages[i].content:
|
if isinstance(messages[i], HumanMessage) and messages[i].content:
|
||||||
@@ -78,52 +85,91 @@ def _extract_activity_summary(messages: list) -> str | None:
|
|||||||
if last_human_idx is None:
|
if last_human_idx is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 本轮交互的消息
|
|
||||||
round_messages = messages[last_human_idx:]
|
round_messages = messages[last_human_idx:]
|
||||||
|
|
||||||
# 提取用户问题
|
# 检查是否为系统心跳消息
|
||||||
user_msg = round_messages[0]
|
user_msg = round_messages[0]
|
||||||
user_content = (
|
user_content = (
|
||||||
user_msg.content if isinstance(user_msg.content, str) else str(user_msg.content)
|
user_msg.content if isinstance(user_msg.content, str) else str(user_msg.content)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 跳过系统心跳消息
|
|
||||||
if user_content.strip().startswith("[System Heartbeat]"):
|
if user_content.strip().startswith("[System Heartbeat]"):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
user_query = user_content.strip()[:100]
|
return round_messages
|
||||||
|
|
||||||
|
|
||||||
|
def _format_conversation_for_summary(round_messages: list) -> str:
|
||||||
|
"""将本轮对话消息格式化为文本,供 LLM 总结。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
round_messages: 本轮交互的消息列表。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
格式化后的对话文本。
|
||||||
|
"""
|
||||||
|
lines = []
|
||||||
|
total_len = 0
|
||||||
|
|
||||||
# 收集本轮交互中使用的工具(仅限本轮)
|
|
||||||
tool_names = set()
|
|
||||||
for msg in round_messages:
|
for msg in round_messages:
|
||||||
if isinstance(msg, AIMessage) and hasattr(msg, "tool_calls") and msg.tool_calls:
|
if isinstance(msg, HumanMessage):
|
||||||
for tc in msg.tool_calls:
|
|
||||||
if isinstance(tc, dict) and "name" in tc:
|
|
||||||
tool_names.add(tc["name"])
|
|
||||||
|
|
||||||
# 提取本轮最后一条 AI 回复的摘要
|
|
||||||
ai_reply = None
|
|
||||||
for msg in reversed(round_messages):
|
|
||||||
if (
|
|
||||||
isinstance(msg, AIMessage)
|
|
||||||
and msg.content
|
|
||||||
and not (hasattr(msg, "tool_calls") and msg.tool_calls)
|
|
||||||
):
|
|
||||||
content = msg.content if isinstance(msg.content, str) else str(msg.content)
|
content = msg.content if isinstance(msg.content, str) else str(msg.content)
|
||||||
ai_reply = content.strip()[:120]
|
line = f"用户: {content}"
|
||||||
|
elif isinstance(msg, AIMessage):
|
||||||
|
if hasattr(msg, "tool_calls") and msg.tool_calls:
|
||||||
|
tool_names = [
|
||||||
|
tc["name"]
|
||||||
|
for tc in msg.tool_calls
|
||||||
|
if isinstance(tc, dict) and "name" in tc
|
||||||
|
]
|
||||||
|
line = f"助手调用工具: {', '.join(tool_names)}"
|
||||||
|
elif msg.content:
|
||||||
|
content = (
|
||||||
|
msg.content if isinstance(msg.content, str) else str(msg.content)
|
||||||
|
)
|
||||||
|
line = f"助手: {content}"
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
elif isinstance(msg, ToolMessage):
|
||||||
|
content = msg.content if isinstance(msg.content, str) else str(msg.content)
|
||||||
|
# 工具返回可能很长,截断
|
||||||
|
if len(content) > 200:
|
||||||
|
content = content[:200] + "..."
|
||||||
|
line = f"工具返回: {content}"
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 控制总长度
|
||||||
|
if total_len + len(line) > MAX_CONTEXT_FOR_SUMMARY:
|
||||||
|
lines.append("...(后续对话省略)")
|
||||||
break
|
break
|
||||||
|
lines.append(line)
|
||||||
|
total_len += len(line)
|
||||||
|
|
||||||
# 组装摘要
|
return "\n".join(lines)
|
||||||
parts = [f"用户: {user_query}"]
|
|
||||||
if tool_names:
|
|
||||||
parts.append(f"工具: {', '.join(sorted(tool_names))}")
|
|
||||||
if ai_reply:
|
|
||||||
parts.append(f"结果: {ai_reply}")
|
|
||||||
|
|
||||||
summary = " | ".join(parts)
|
|
||||||
if len(summary) > MAX_ENTRY_LENGTH:
|
async def _summarize_with_llm(conversation_text: str) -> str | None:
|
||||||
summary = summary[: MAX_ENTRY_LENGTH - 3] + "..."
|
"""调用 LLM 对对话文本生成活动摘要。
|
||||||
return summary
|
|
||||||
|
参数:
|
||||||
|
conversation_text: 格式化后的对话文本。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
LLM 生成的摘要字符串,失败时返回 None。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from app.helper.llm import LLMHelper
|
||||||
|
|
||||||
|
llm = LLMHelper.get_llm(streaming=False)
|
||||||
|
prompt = SUMMARY_PROMPT.format(conversation=conversation_text)
|
||||||
|
response = await llm.ainvoke(prompt)
|
||||||
|
summary = response.content.strip()
|
||||||
|
# 清理模型可能输出的前缀(如 "摘要:" "总结:")
|
||||||
|
summary = re.sub(r"^(摘要|总结|活动记录)[::]\s*", "", summary)
|
||||||
|
return summary if summary else None
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("LLM summarization failed: %s", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
ACTIVITY_LOG_SYSTEM_PROMPT = """<activity_log>
|
ACTIVITY_LOG_SYSTEM_PROMPT = """<activity_log>
|
||||||
@@ -331,14 +377,24 @@ class ActivityLogMiddleware(AgentMiddleware[ActivityLogState, ContextT, Response
|
|||||||
async def aafter_agent(
|
async def aafter_agent(
|
||||||
self, state: ActivityLogState, runtime: Runtime
|
self, state: ActivityLogState, runtime: Runtime
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
"""Agent 执行完毕后,从对话中提取摘要并追加到当日活动日志。"""
|
"""Agent 执行完毕后,调用 LLM 对本轮对话生成摘要并追加到当日活动日志。"""
|
||||||
try:
|
try:
|
||||||
messages = state.get("messages", [])
|
messages = state.get("messages", [])
|
||||||
if not messages:
|
if not messages:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 提取活动摘要
|
# 提取本轮交互
|
||||||
summary = _extract_activity_summary(messages)
|
round_messages = _extract_last_round(messages)
|
||||||
|
if not round_messages:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 格式化对话文本
|
||||||
|
conversation_text = _format_conversation_for_summary(round_messages)
|
||||||
|
if not conversation_text:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 调用 LLM 生成摘要
|
||||||
|
summary = await _summarize_with_llm(conversation_text)
|
||||||
if summary:
|
if summary:
|
||||||
await self._append_activity(summary)
|
await self._append_activity(summary)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user