|
@@ -26,7 +26,6 @@ from typing import AsyncIterator, Optional, Dict, Any, List, Callable, Literal,
|
|
|
from agent.trace.models import Trace, Message
|
|
from agent.trace.models import Trace, Message
|
|
|
from agent.trace.protocols import TraceStore
|
|
from agent.trace.protocols import TraceStore
|
|
|
from agent.trace.goal_models import GoalTree
|
|
from agent.trace.goal_models import GoalTree
|
|
|
-from agent.tools.builtin.knowledge import knowledge_save, knowledge_batch_update
|
|
|
|
|
from agent.trace.compaction import (
|
|
from agent.trace.compaction import (
|
|
|
CompressionConfig,
|
|
CompressionConfig,
|
|
|
filter_by_goal_status,
|
|
filter_by_goal_status,
|
|
@@ -46,17 +45,42 @@ from agent.core.prompts import (
|
|
|
AGENT_CONTINUE_HINT_TEMPLATE,
|
|
AGENT_CONTINUE_HINT_TEMPLATE,
|
|
|
TASK_NAME_GENERATION_SYSTEM_PROMPT,
|
|
TASK_NAME_GENERATION_SYSTEM_PROMPT,
|
|
|
TASK_NAME_FALLBACK,
|
|
TASK_NAME_FALLBACK,
|
|
|
- EXPERIENCE_PARSE_WARNING,
|
|
|
|
|
SUMMARY_HEADER_TEMPLATE,
|
|
SUMMARY_HEADER_TEMPLATE,
|
|
|
|
|
+ COMPLETION_REFLECT_PROMPT,
|
|
|
build_summary_header,
|
|
build_summary_header,
|
|
|
build_tool_interrupted_message,
|
|
build_tool_interrupted_message,
|
|
|
build_agent_continue_hint,
|
|
build_agent_continue_hint,
|
|
|
- build_experience_entry,
|
|
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+# ===== 知识管理配置 =====
|
|
|
|
|
+
|
|
|
|
|
+@dataclass
|
|
|
|
|
+class KnowledgeConfig:
|
|
|
|
|
+ """知识提取与注入的配置"""
|
|
|
|
|
+
|
|
|
|
|
+ # 压缩时提取(消息量超阈值触发压缩时,在 Level 1 过滤前用完整 history 反思)
|
|
|
|
|
+ enable_extraction: bool = True # 是否在压缩触发时提取知识
|
|
|
|
|
+ reflect_prompt: str = "" # 自定义反思 prompt;空则使用默认,见 agent/core/prompts/knowledge.py:REFLECT_PROMPT
|
|
|
|
|
+
|
|
|
|
|
+ # agent运行完成后提取(不代表任务完成,agent 可能中途退出等待人工评估)
|
|
|
|
|
+ enable_completion_extraction: bool = True # 是否在运行完成后提取知识
|
|
|
|
|
+ completion_reflect_prompt: str = "" # 自定义复盘 prompt;空则使用默认,见 agent/core/prompts/knowledge.py:COMPLETION_REFLECT_PROMPT
|
|
|
|
|
+
|
|
|
|
|
+ # 知识注入(agent切换当前工作的goal时,自动注入相关知识)
|
|
|
|
|
+ enable_injection: bool = True # 是否在 focus goal 时自动注入相关知识
|
|
|
|
|
+
|
|
|
|
|
+ def get_reflect_prompt(self) -> str:
|
|
|
|
|
+ """压缩时反思 prompt"""
|
|
|
|
|
+ return self.reflect_prompt if self.reflect_prompt else build_reflect_prompt()
|
|
|
|
|
+
|
|
|
|
|
+ def get_completion_reflect_prompt(self) -> str:
|
|
|
|
|
+ """任务完成后复盘 prompt"""
|
|
|
|
|
+ return self.completion_reflect_prompt if self.completion_reflect_prompt else COMPLETION_REFLECT_PROMPT
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
# ===== 运行配置 =====
|
|
# ===== 运行配置 =====
|
|
|
|
|
|
|
|
@dataclass
|
|
@dataclass
|
|
@@ -93,8 +117,8 @@ class RunConfig:
|
|
|
# --- 额外 LLM 参数(传给 llm_call 的 **kwargs)---
|
|
# --- 额外 LLM 参数(传给 llm_call 的 **kwargs)---
|
|
|
extra_llm_params: Dict[str, Any] = field(default_factory=dict)
|
|
extra_llm_params: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
|
|
- # --- 研究流程控制 ---
|
|
|
|
|
- enable_research_flow: bool = False # 已废弃,保留字段避免调用方传参报错
|
|
|
|
|
|
|
+ # --- 知识管理配置 ---
|
|
|
|
|
+ knowledge: KnowledgeConfig = field(default_factory=KnowledgeConfig)
|
|
|
|
|
|
|
|
|
|
|
|
|
# 内置工具列表(始终自动加载)
|
|
# 内置工具列表(始终自动加载)
|
|
@@ -685,6 +709,14 @@ class AgentRunner:
|
|
|
else:
|
|
else:
|
|
|
print(f"[压缩评估] ✅ 未超阈值,无需压缩")
|
|
print(f"[压缩评估] ✅ 未超阈值,无需压缩")
|
|
|
|
|
|
|
|
|
|
+ # 知识提取:在任何压缩发生前,用完整 history 做反思
|
|
|
|
|
+ if needs_compression and config.knowledge.enable_extraction:
|
|
|
|
|
+ await self._run_reflect(
|
|
|
|
|
+ trace_id, history, config,
|
|
|
|
|
+ reflect_prompt=config.knowledge.get_reflect_prompt(),
|
|
|
|
|
+ source_name="compression_reflection",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
if needs_compression and self.trace_store and goal_tree:
|
|
if needs_compression and self.trace_store and goal_tree:
|
|
|
# 使用本地 head_seq(store 中的 head_sequence 在 loop 期间未更新,是过时的)
|
|
# 使用本地 head_seq(store 中的 head_sequence 在 loop 期间未更新,是过时的)
|
|
|
if head_seq > 0:
|
|
if head_seq > 0:
|
|
@@ -1009,6 +1041,10 @@ class AgentRunner:
|
|
|
# 无工具调用,任务完成
|
|
# 无工具调用,任务完成
|
|
|
break
|
|
break
|
|
|
|
|
|
|
|
|
|
+ # 任务完成后复盘提取
|
|
|
|
|
+ if config.knowledge.enable_completion_extraction:
|
|
|
|
|
+ await self._extract_knowledge_on_completion(trace_id, history, config)
|
|
|
|
|
+
|
|
|
# 更新 head_sequence 并完成 Trace
|
|
# 更新 head_sequence 并完成 Trace
|
|
|
if self.trace_store:
|
|
if self.trace_store:
|
|
|
await self.trace_store.update_trace(
|
|
await self.trace_store.update_trace(
|
|
@@ -1035,10 +1071,9 @@ class AgentRunner:
|
|
|
"""
|
|
"""
|
|
|
Level 2 压缩:LLM 总结
|
|
Level 2 压缩:LLM 总结
|
|
|
|
|
|
|
|
- Step 1: 经验提取(reflect)— 纯内存 LLM 调用 + 文件追加,不影响 trace
|
|
|
|
|
- Step 2: 压缩总结 — LLM 生成 summary
|
|
|
|
|
- Step 3: 存储 summary 为新消息,parent_sequence 跳到 system msg
|
|
|
|
|
- Step 4: 重建 history
|
|
|
|
|
|
|
+ Step 1: 压缩总结 — LLM 生成 summary
|
|
|
|
|
+ Step 2: 存储 summary 为新消息,parent_sequence 跳到 system msg
|
|
|
|
|
+ Step 3: 重建 history
|
|
|
|
|
|
|
|
Returns:
|
|
Returns:
|
|
|
(new_history, new_head_seq, next_sequence)
|
|
(new_history, new_head_seq, next_sequence)
|
|
@@ -1071,129 +1106,7 @@ class AgentRunner:
|
|
|
logger.warning("Level 2 压缩跳过:未找到 system message")
|
|
logger.warning("Level 2 压缩跳过:未找到 system message")
|
|
|
return history, head_seq, sequence
|
|
return history, head_seq, sequence
|
|
|
|
|
|
|
|
- # --- Step 1: 经验提取(reflect)---
|
|
|
|
|
- try:
|
|
|
|
|
- # 1. 构造 Reflect Prompt(确保包含格式要求)
|
|
|
|
|
- # 建议在 build_reflect_prompt() 里加入:
|
|
|
|
|
- # "请使用格式:- [intent: 意图, state: 状态描述] 具体的经验内容"
|
|
|
|
|
- reflect_prompt = build_reflect_prompt()
|
|
|
|
|
- reflect_messages = list(history) + [{"role": "user", "content": reflect_prompt}]
|
|
|
|
|
-
|
|
|
|
|
- # 应用 Prompt Caching
|
|
|
|
|
- reflect_messages = self._add_cache_control(
|
|
|
|
|
- reflect_messages,
|
|
|
|
|
- config.model,
|
|
|
|
|
- config.enable_prompt_caching
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- reflect_result = await self.llm_call(
|
|
|
|
|
- messages=reflect_messages,
|
|
|
|
|
- model=config.model,
|
|
|
|
|
- tools=[],
|
|
|
|
|
- temperature=0.2, # 略微保持一点发散性
|
|
|
|
|
- **config.extra_llm_params,
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- reflection_text = reflect_result.get("content", "").strip()
|
|
|
|
|
-
|
|
|
|
|
- if reflection_text:
|
|
|
|
|
- import re as _re2
|
|
|
|
|
- import uuid as _uuid2
|
|
|
|
|
-
|
|
|
|
|
- pattern = r"-\s*\[(?P<tags>.*?)\]\s*(?P<content>.*)"
|
|
|
|
|
- matches = list(_re2.finditer(pattern, reflection_text))
|
|
|
|
|
-
|
|
|
|
|
- structured_entries = []
|
|
|
|
|
- for match in matches:
|
|
|
|
|
- tags_str = match.group("tags")
|
|
|
|
|
- content = match.group("content")
|
|
|
|
|
-
|
|
|
|
|
- intent_match = _re2.search(r"intent:\s*(.*?)(?:,|$)", tags_str, _re2.IGNORECASE)
|
|
|
|
|
- state_match = _re2.search(r"state:\s*(.*?)(?:,|$)", tags_str, _re2.IGNORECASE)
|
|
|
|
|
-
|
|
|
|
|
- intents = [i.strip() for i in intent_match.group(1).split(",")] if intent_match and intent_match.group(1) else []
|
|
|
|
|
- states = [s.strip() for s in state_match.group(1).split(",")] if state_match and state_match.group(1) else []
|
|
|
|
|
-
|
|
|
|
|
- ex_id = f"ex_{datetime.now().strftime('%m%d%H%M')}_{_uuid2.uuid4().hex[:4]}"
|
|
|
|
|
- entry = build_experience_entry(
|
|
|
|
|
- ex_id=ex_id,
|
|
|
|
|
- trace_id=trace_id,
|
|
|
|
|
- intents=intents,
|
|
|
|
|
- states=states,
|
|
|
|
|
- created_at=datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
|
|
|
|
- content=f"- {content}\n- 经验ID: [{ex_id}]"
|
|
|
|
|
- )
|
|
|
|
|
- structured_entries.append(entry)
|
|
|
|
|
-
|
|
|
|
|
- if structured_entries:
|
|
|
|
|
- # 保存经验为知识(strategy 标签)
|
|
|
|
|
- saved_count = 0
|
|
|
|
|
- for entry in structured_entries:
|
|
|
|
|
- try:
|
|
|
|
|
- # 从 entry 中提取信息
|
|
|
|
|
- lines = entry.split("\n")
|
|
|
|
|
- ex_id = ""
|
|
|
|
|
- intents = []
|
|
|
|
|
- states = []
|
|
|
|
|
- content = ""
|
|
|
|
|
-
|
|
|
|
|
- for line in lines:
|
|
|
|
|
- if line.startswith("id:"):
|
|
|
|
|
- ex_id = line.split(":", 1)[1].strip()
|
|
|
|
|
- elif line.startswith("tags:"):
|
|
|
|
|
- tags_match = _re2.search(r"intent:\s*\[(.*?)\].*state:\s*\[(.*?)\]", line)
|
|
|
|
|
- if tags_match:
|
|
|
|
|
- intents_str = tags_match.group(1).strip("'\"")
|
|
|
|
|
- states_str = tags_match.group(2).strip("'\"")
|
|
|
|
|
- intents = [i.strip().strip("'\"") for i in intents_str.split(",") if i.strip()]
|
|
|
|
|
- states = [s.strip().strip("'\"") for s in states_str.split(",") if s.strip()]
|
|
|
|
|
- elif line.startswith("- ") and not line.startswith("- 经验ID:"):
|
|
|
|
|
- content = line[2:].strip()
|
|
|
|
|
-
|
|
|
|
|
- # 构建 task(从 intent 和 state 生成)
|
|
|
|
|
- task_parts = []
|
|
|
|
|
- if intents:
|
|
|
|
|
- task_parts.append(f"意图: {', '.join(intents)}")
|
|
|
|
|
- if states:
|
|
|
|
|
- task_parts.append(f"状态: {', '.join(states)}")
|
|
|
|
|
- task = " | ".join(task_parts) if task_parts else "通用经验"
|
|
|
|
|
-
|
|
|
|
|
- # 构建 tags(将 intents 和 states 作为业务标签)
|
|
|
|
|
- tags = {}
|
|
|
|
|
- if intents:
|
|
|
|
|
- tags["intent"] = ", ".join(intents)
|
|
|
|
|
- if states:
|
|
|
|
|
- tags["state"] = ", ".join(states)
|
|
|
|
|
-
|
|
|
|
|
- # 调用 knowledge_save 保存为 strategy 类型的知识
|
|
|
|
|
- result = await knowledge_save(
|
|
|
|
|
- task=task,
|
|
|
|
|
- content=content,
|
|
|
|
|
- types=["strategy"],
|
|
|
|
|
- tags=tags,
|
|
|
|
|
- urls=[],
|
|
|
|
|
- agent_id="runner",
|
|
|
|
|
- source_name="compression_reflection",
|
|
|
|
|
- source_category="exp",
|
|
|
|
|
- score=3,
|
|
|
|
|
- message_id=trace_id # 使用 trace_id 作为 message_id
|
|
|
|
|
- )
|
|
|
|
|
- saved_count += 1
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- logger.warning(f"保存经验失败: {e}")
|
|
|
|
|
- continue
|
|
|
|
|
-
|
|
|
|
|
- logger.info(f"已提取并保存 {saved_count}/{len(structured_entries)} 条结构化经验到知识库")
|
|
|
|
|
- else:
|
|
|
|
|
- logger.warning(EXPERIENCE_PARSE_WARNING)
|
|
|
|
|
- logger.debug(f"LLM Raw Output:\n{reflection_text}")
|
|
|
|
|
- else:
|
|
|
|
|
- logger.warning("LLM 未生成反思内容")
|
|
|
|
|
-
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- logger.error(f"Level 2 经验提取失败: {e}")
|
|
|
|
|
-
|
|
|
|
|
- # --- Step 2: 压缩总结 ---
|
|
|
|
|
|
|
+ # --- Step 1: 压缩总结 ---
|
|
|
compress_prompt = build_compression_prompt(goal_tree)
|
|
compress_prompt = build_compression_prompt(goal_tree)
|
|
|
compress_messages = list(history) + [{"role": "user", "content": compress_prompt}]
|
|
compress_messages = list(history) + [{"role": "user", "content": compress_prompt}]
|
|
|
|
|
|
|
@@ -1261,6 +1174,91 @@ class AgentRunner:
|
|
|
|
|
|
|
|
return new_history, new_head_seq, sequence
|
|
return new_history, new_head_seq, sequence
|
|
|
|
|
|
|
|
|
|
+ async def _run_reflect(
|
|
|
|
|
+ self,
|
|
|
|
|
+ trace_id: str,
|
|
|
|
|
+ history: List[Dict],
|
|
|
|
|
+ config: RunConfig,
|
|
|
|
|
+ reflect_prompt: str,
|
|
|
|
|
+ source_name: str,
|
|
|
|
|
+ ) -> None:
|
|
|
|
|
+ """
|
|
|
|
|
+ 执行反思提取:LLM 对历史消息进行反思,直接调用 knowledge_save 工具保存经验。
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ trace_id: Trace ID(作为知识的 message_id)
|
|
|
|
|
+ history: 当前对话历史
|
|
|
|
|
+ config: 运行配置
|
|
|
|
|
+ reflect_prompt: 反思 prompt
|
|
|
|
|
+ source_name: 来源名称(用于区分压缩时/完成时)
|
|
|
|
|
+ """
|
|
|
|
|
+ try:
|
|
|
|
|
+ reflect_messages = list(history) + [{"role": "user", "content": reflect_prompt}]
|
|
|
|
|
+ reflect_messages = self._add_cache_control(
|
|
|
|
|
+ reflect_messages, config.model, config.enable_prompt_caching
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # 只暴露 knowledge_save 工具,让 LLM 直接调用
|
|
|
|
|
+ knowledge_save_schema = self._get_tool_schemas(["knowledge_save"])
|
|
|
|
|
+
|
|
|
|
|
+ reflect_result = await self.llm_call(
|
|
|
|
|
+ messages=reflect_messages,
|
|
|
|
|
+ model=config.model,
|
|
|
|
|
+ tools=knowledge_save_schema,
|
|
|
|
|
+ temperature=0.2,
|
|
|
|
|
+ **config.extra_llm_params,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ tool_calls = reflect_result.get("tool_calls") or []
|
|
|
|
|
+ if not tool_calls:
|
|
|
|
|
+ logger.info("反思阶段无经验保存 (source=%s)", source_name)
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ saved_count = 0
|
|
|
|
|
+ for tc in tool_calls:
|
|
|
|
|
+ tool_name = tc.get("function", {}).get("name")
|
|
|
|
|
+ if tool_name != "knowledge_save":
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ tool_args = tc.get("function", {}).get("arguments") or {}
|
|
|
|
|
+ if isinstance(tool_args, str):
|
|
|
|
|
+ tool_args = json.loads(tool_args) if tool_args.strip() else {}
|
|
|
|
|
+
|
|
|
|
|
+ # 注入来源信息(LLM 不需要填写这些字段)
|
|
|
|
|
+ tool_args.setdefault("source_name", source_name)
|
|
|
|
|
+ tool_args.setdefault("source_category", "exp")
|
|
|
|
|
+ tool_args.setdefault("message_id", trace_id)
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ await self.tools.execute(
|
|
|
|
|
+ "knowledge_save",
|
|
|
|
|
+ tool_args,
|
|
|
|
|
+ uid=config.uid or "",
|
|
|
|
|
+ context={"store": self.trace_store, "trace_id": trace_id},
|
|
|
|
|
+ )
|
|
|
|
|
+ saved_count += 1
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.warning("保存经验失败: %s", e)
|
|
|
|
|
+
|
|
|
|
|
+ logger.info("已提取并保存 %d 条经验 (source=%s)", saved_count, source_name)
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error("知识反思提取失败 (source=%s): %s", source_name, e)
|
|
|
|
|
+
|
|
|
|
|
+ async def _extract_knowledge_on_completion(
|
|
|
|
|
+ self,
|
|
|
|
|
+ trace_id: str,
|
|
|
|
|
+ history: List[Dict],
|
|
|
|
|
+ config: RunConfig,
|
|
|
|
|
+ ) -> None:
|
|
|
|
|
+ """任务完成后执行全局复盘,提取经验保存到知识库。"""
|
|
|
|
|
+ logger.info("任务完成后复盘提取: trace=%s", trace_id)
|
|
|
|
|
+ await self._run_reflect(
|
|
|
|
|
+ trace_id, history, config,
|
|
|
|
|
+ reflect_prompt=config.knowledge.get_completion_reflect_prompt(),
|
|
|
|
|
+ source_name="completion_reflection",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
# ===== 回溯(Rewind)=====
|
|
# ===== 回溯(Rewind)=====
|
|
|
|
|
|
|
|
async def _rewind(
|
|
async def _rewind(
|