|
@@ -67,8 +67,8 @@ class ContextUsage:
|
|
|
|
|
|
|
|
@dataclass
|
|
@dataclass
|
|
|
class SideBranchContext:
|
|
class SideBranchContext:
|
|
|
- """侧分支上下文(压缩/反思)"""
|
|
|
|
|
- type: Literal["compression", "reflection"]
|
|
|
|
|
|
|
+ """侧分支上下文(压缩/反思/知识评估)"""
|
|
|
|
|
+ type: Literal["compression", "reflection", "knowledge_eval"]
|
|
|
branch_id: str
|
|
branch_id: str
|
|
|
start_head_seq: int # 侧分支起点的 head_seq
|
|
start_head_seq: int # 侧分支起点的 head_seq
|
|
|
start_sequence: int # 侧分支第一条消息的 sequence
|
|
start_sequence: int # 侧分支第一条消息的 sequence
|
|
@@ -783,6 +783,28 @@ class AgentRunner:
|
|
|
if not needs_compression:
|
|
if not needs_compression:
|
|
|
return history, head_seq, sequence, False
|
|
return history, head_seq, sequence, False
|
|
|
|
|
|
|
|
|
|
+ # 检查是否有待评估知识(压缩前必须先评估)
|
|
|
|
|
+ if self.trace_store and not config.force_side_branch:
|
|
|
|
|
+ pending = await self.trace_store.get_pending_knowledge_entries(trace_id)
|
|
|
|
|
+ if pending:
|
|
|
|
|
+ # 设置侧分支队列:反思 → 知识评估 → 压缩
|
|
|
|
|
+ # 反思放在前面,确保反思期间完成的 goal 产生的新知识也能在压缩前被评估
|
|
|
|
|
+ if config.knowledge.enable_extraction:
|
|
|
|
|
+ config.force_side_branch = ["reflection", "knowledge_eval", "compression"]
|
|
|
|
|
+ else:
|
|
|
|
|
+ config.force_side_branch = ["knowledge_eval", "compression"]
|
|
|
|
|
+
|
|
|
|
|
+ # 在 trace.context 中设置触发事件
|
|
|
|
|
+ trace = await self.trace_store.get_trace(trace_id)
|
|
|
|
|
+ if trace:
|
|
|
|
|
+ if not trace.context:
|
|
|
|
|
+ trace.context = {}
|
|
|
|
|
+ trace.context["knowledge_eval_trigger"] = "compression"
|
|
|
|
|
+ await self.trace_store.update_trace(trace_id, context=trace.context)
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(f"[Knowledge Eval] 压缩前触发知识评估,待评估: {len(pending)} 条")
|
|
|
|
|
+ return history, head_seq, sequence, True
|
|
|
|
|
+
|
|
|
# 知识提取:在任何压缩发生前,用完整 history 做反思(进入反思侧分支)
|
|
# 知识提取:在任何压缩发生前,用完整 history 做反思(进入反思侧分支)
|
|
|
if config.knowledge.enable_extraction and not config.force_side_branch:
|
|
if config.knowledge.enable_extraction and not config.force_side_branch:
|
|
|
# 设置侧分支队列:先反思,再压缩
|
|
# 设置侧分支队列:先反思,再压缩
|
|
@@ -846,6 +868,74 @@ class AgentRunner:
|
|
|
|
|
|
|
|
return history, head_seq, sequence, False
|
|
return history, head_seq, sequence, False
|
|
|
|
|
|
|
|
|
|
+ async def _build_knowledge_eval_prompt(
|
|
|
|
|
+ self,
|
|
|
|
|
+ trace_id: str,
|
|
|
|
|
+ goal_tree: Optional[GoalTree]
|
|
|
|
|
+ ) -> str:
|
|
|
|
|
+ """构建知识评估 prompt"""
|
|
|
|
|
+ if not self.trace_store:
|
|
|
|
|
+ return ""
|
|
|
|
|
+
|
|
|
|
|
+ pending = await self.trace_store.get_pending_knowledge_entries(trace_id)
|
|
|
|
|
+ if not pending:
|
|
|
|
|
+ return ""
|
|
|
|
|
+
|
|
|
|
|
+ # 获取mission
|
|
|
|
|
+ trace = await self.trace_store.get_trace(trace_id)
|
|
|
|
|
+ mission = trace.task if trace else "未知任务"
|
|
|
|
|
+
|
|
|
|
|
+ # 获取当前Goal
|
|
|
|
|
+ current_goal = goal_tree.find(goal_tree.current_id) if goal_tree and goal_tree.current_id else None
|
|
|
|
|
+ goal_desc = current_goal.description if current_goal else "无当前目标"
|
|
|
|
|
+
|
|
|
|
|
+ # 构建知识列表
|
|
|
|
|
+ knowledge_list = []
|
|
|
|
|
+ for idx, entry in enumerate(pending, 1):
|
|
|
|
|
+ knowledge_list.append(
|
|
|
|
|
+ f"### 知识 {idx}: {entry['knowledge_id']}\n"
|
|
|
|
|
+ f"- task: {entry['task']}\n"
|
|
|
|
|
+ f"- content: {entry['content']}\n"
|
|
|
|
|
+ f"- 注入于: sequence {entry['injected_at_sequence']}, goal {entry['goal_id']}"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ prompt = f"""你是知识评估助手。请评估以下知识在本次任务执行中的实际效果。
|
|
|
|
|
+
|
|
|
|
|
+## 当前任务(Mission)
|
|
|
|
|
+{mission}
|
|
|
|
|
+
|
|
|
|
|
+## 当前 Goal
|
|
|
|
|
+{goal_desc}
|
|
|
|
|
+
|
|
|
|
|
+## 待评估知识列表
|
|
|
|
|
+{chr(10).join(knowledge_list)}
|
|
|
|
|
+
|
|
|
|
|
+## 评估维度
|
|
|
|
|
+1. **helpfulness**: 知识内容是否对完成任务有实质帮助?
|
|
|
|
|
+2. **relevance**: 执行过程中是否体现了该知识的内容?
|
|
|
|
|
+
|
|
|
|
|
+## 评估分类
|
|
|
|
|
+- irrelevant: task与当前任务无关
|
|
|
|
|
+- unused: 相关但未使用
|
|
|
|
|
+- helpful: 有帮助
|
|
|
|
|
+- harmful: 有负面作用
|
|
|
|
|
+- neutral: 无明显作用
|
|
|
|
|
+
|
|
|
|
|
+## 输出格式
|
|
|
|
|
+请直接输出评估结果,使用JSON格式:
|
|
|
|
|
+
|
|
|
|
|
+{{
|
|
|
|
|
+ "evaluations": [
|
|
|
|
|
+ {{
|
|
|
|
|
+ "knowledge_id": "knowledge-xxx",
|
|
|
|
|
+ "eval_status": "helpful",
|
|
|
|
|
+ "reason": "1-2句评估理由"
|
|
|
|
|
+ }}
|
|
|
|
|
+ ]
|
|
|
|
|
+}}
|
|
|
|
|
+"""
|
|
|
|
|
+ return prompt
|
|
|
|
|
+
|
|
|
async def _single_turn_compress(
|
|
async def _single_turn_compress(
|
|
|
self,
|
|
self,
|
|
|
trace_id: str,
|
|
trace_id: str,
|
|
@@ -1052,6 +1142,17 @@ class AgentRunner:
|
|
|
yield trace_obj
|
|
yield trace_obj
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
|
|
+ # 检查Goal完成触发的知识评估
|
|
|
|
|
+ if not side_branch_ctx and self.trace_store:
|
|
|
|
|
+ trace = await self.trace_store.get_trace(trace_id)
|
|
|
|
|
+ if trace and trace.context and trace.context.get("pending_knowledge_eval"):
|
|
|
|
|
+ # 清除标志
|
|
|
|
|
+ trace.context.pop("pending_knowledge_eval", None)
|
|
|
|
|
+ await self.trace_store.update_trace(trace_id, context=trace.context)
|
|
|
|
|
+ # 设置侧分支队列
|
|
|
|
|
+ config.force_side_branch = ["knowledge_eval"]
|
|
|
|
|
+ logger.info("[Knowledge Eval] 检测到Goal完成触发,进入知识评估侧分支")
|
|
|
|
|
+
|
|
|
# Context 管理(仅主路径)
|
|
# Context 管理(仅主路径)
|
|
|
needs_enter_side_branch = False
|
|
needs_enter_side_branch = False
|
|
|
if not side_branch_ctx:
|
|
if not side_branch_ctx:
|
|
@@ -1071,9 +1172,15 @@ class AgentRunner:
|
|
|
|
|
|
|
|
# 进入侧分支
|
|
# 进入侧分支
|
|
|
if needs_enter_side_branch and not side_branch_ctx:
|
|
if needs_enter_side_branch and not side_branch_ctx:
|
|
|
|
|
+ # 刷新 trace,获取 _manage_context_usage 可能写入 DB 的 knowledge_eval_trigger
|
|
|
|
|
+ if self.trace_store:
|
|
|
|
|
+ fresh = await self.trace_store.get_trace(trace_id)
|
|
|
|
|
+ if fresh:
|
|
|
|
|
+ trace = fresh
|
|
|
# 从队列中取出第一个侧分支类型
|
|
# 从队列中取出第一个侧分支类型
|
|
|
|
|
+ branch_type: Literal["compression", "reflection", "knowledge_eval"]
|
|
|
if config.force_side_branch and isinstance(config.force_side_branch, list) and len(config.force_side_branch) > 0:
|
|
if config.force_side_branch and isinstance(config.force_side_branch, list) and len(config.force_side_branch) > 0:
|
|
|
- branch_type = config.force_side_branch.pop(0)
|
|
|
|
|
|
|
+ branch_type = config.force_side_branch.pop(0) # type: ignore
|
|
|
logger.info(f"从队列取出侧分支: {branch_type}, 剩余队列: {config.force_side_branch}")
|
|
logger.info(f"从队列取出侧分支: {branch_type}, 剩余队列: {config.force_side_branch}")
|
|
|
elif config.knowledge.enable_extraction:
|
|
elif config.knowledge.enable_extraction:
|
|
|
# 兼容旧的单值模式(如果 force_side_branch 是字符串)
|
|
# 兼容旧的单值模式(如果 force_side_branch 是字符串)
|
|
@@ -1096,6 +1203,9 @@ class AgentRunner:
|
|
|
|
|
|
|
|
# 持久化侧分支状态
|
|
# 持久化侧分支状态
|
|
|
if self.trace_store:
|
|
if self.trace_store:
|
|
|
|
|
+ # 获取触发事件(如果是 knowledge_eval 分支)
|
|
|
|
|
+ trigger_event = trace.context.get("knowledge_eval_trigger", "unknown") if branch_type == "knowledge_eval" else None
|
|
|
|
|
+
|
|
|
trace.context["active_side_branch"] = {
|
|
trace.context["active_side_branch"] = {
|
|
|
"type": side_branch_ctx.type,
|
|
"type": side_branch_ctx.type,
|
|
|
"branch_id": side_branch_ctx.branch_id,
|
|
"branch_id": side_branch_ctx.branch_id,
|
|
@@ -1105,6 +1215,13 @@ class AgentRunner:
|
|
|
"max_turns": side_branch_ctx.max_turns,
|
|
"max_turns": side_branch_ctx.max_turns,
|
|
|
"started_at": datetime.now().isoformat(),
|
|
"started_at": datetime.now().isoformat(),
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ # 如果是 knowledge_eval 分支,添加 trigger_event
|
|
|
|
|
+ if trigger_event:
|
|
|
|
|
+ trace.context["active_side_branch"]["trigger_event"] = trigger_event
|
|
|
|
|
+ # 清除触发事件标记
|
|
|
|
|
+ trace.context.pop("knowledge_eval_trigger", None)
|
|
|
|
|
+
|
|
|
await self.trace_store.update_trace(
|
|
await self.trace_store.update_trace(
|
|
|
trace_id,
|
|
trace_id,
|
|
|
context=trace.context
|
|
context=trace.context
|
|
@@ -1113,6 +1230,8 @@ class AgentRunner:
|
|
|
# 追加侧分支 prompt
|
|
# 追加侧分支 prompt
|
|
|
if branch_type == "reflection":
|
|
if branch_type == "reflection":
|
|
|
prompt = config.knowledge.get_reflect_prompt()
|
|
prompt = config.knowledge.get_reflect_prompt()
|
|
|
|
|
+ elif branch_type == "knowledge_eval":
|
|
|
|
|
+ prompt = await self._build_knowledge_eval_prompt(trace_id, goal_tree)
|
|
|
else: # compression
|
|
else: # compression
|
|
|
from agent.trace.compaction import build_compression_prompt
|
|
from agent.trace.compaction import build_compression_prompt
|
|
|
prompt = build_compression_prompt(goal_tree)
|
|
prompt = build_compression_prompt(goal_tree)
|
|
@@ -1249,6 +1368,48 @@ class AgentRunner:
|
|
|
cache_read_tokens=cache_read_tokens or 0,
|
|
cache_read_tokens=cache_read_tokens or 0,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+ # 知识评估侧分支:即时检测并写入评估结果
|
|
|
|
|
+ if side_branch_ctx and side_branch_ctx.type == "knowledge_eval":
|
|
|
|
|
+ text = response_content if isinstance(response_content, str) else ""
|
|
|
|
|
+ eval_results = None
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ eval_results = json.loads(text.strip())
|
|
|
|
|
+ if "evaluations" not in eval_results:
|
|
|
|
|
+ eval_results = None
|
|
|
|
|
+ except json.JSONDecodeError:
|
|
|
|
|
+ import re
|
|
|
|
|
+ json_match = re.search(r'```json\s*(\{.*?\})\s*```', text, re.DOTALL)
|
|
|
|
|
+ if json_match:
|
|
|
|
|
+ try:
|
|
|
|
|
+ eval_results = json.loads(json_match.group(1))
|
|
|
|
|
+ except json.JSONDecodeError:
|
|
|
|
|
+ pass
|
|
|
|
|
+
|
|
|
|
|
+ if not eval_results:
|
|
|
|
|
+ json_match = re.search(r'\{[^{]*"evaluations"[^}]*\[[^\]]*\][^}]*\}', text, re.DOTALL)
|
|
|
|
|
+ if json_match:
|
|
|
|
|
+ try:
|
|
|
|
|
+ eval_results = json.loads(json_match.group(0))
|
|
|
|
|
+ except json.JSONDecodeError:
|
|
|
|
|
+ pass
|
|
|
|
|
+
|
|
|
|
|
+ if eval_results and self.trace_store:
|
|
|
|
|
+ current_trace = await self.trace_store.get_trace(trace_id)
|
|
|
|
|
+ trigger_event = current_trace.context.get("active_side_branch", {}).get("trigger_event", "unknown")
|
|
|
|
|
+
|
|
|
|
|
+ for eval_item in eval_results.get("evaluations", []):
|
|
|
|
|
+ await self.trace_store.update_knowledge_evaluation(
|
|
|
|
|
+ trace_id=trace_id,
|
|
|
|
|
+ knowledge_id=eval_item["knowledge_id"],
|
|
|
|
|
+ eval_result={
|
|
|
|
|
+ "eval_status": eval_item["eval_status"],
|
|
|
|
|
+ "reason": eval_item.get("reason", "")
|
|
|
|
|
+ },
|
|
|
|
|
+ trigger_event=trigger_event
|
|
|
|
|
+ )
|
|
|
|
|
+ logger.info(f"[Knowledge Eval] 已写入 {len(eval_results.get('evaluations', []))} 条评估结果")
|
|
|
|
|
+
|
|
|
# 如果在侧分支,记录到 assistant_msg(已持久化,不需要额外维护)
|
|
# 如果在侧分支,记录到 assistant_msg(已持久化,不需要额外维护)
|
|
|
|
|
|
|
|
yield assistant_msg
|
|
yield assistant_msg
|
|
@@ -1386,6 +1547,30 @@ class AgentRunner:
|
|
|
side_branch_ctx = None
|
|
side_branch_ctx = None
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
|
|
+ elif should_exit and side_branch_ctx.type == "knowledge_eval":
|
|
|
|
|
+ # === 知识评估侧分支退出 ===
|
|
|
|
|
+ logger.info("知识评估侧分支退出")
|
|
|
|
|
+
|
|
|
|
|
+ # 恢复主路径
|
|
|
|
|
+ if self.trace_store:
|
|
|
|
|
+ main_path_messages = await self.trace_store.get_main_path_messages(
|
|
|
|
|
+ trace_id, side_branch_ctx.start_head_seq
|
|
|
|
|
+ )
|
|
|
|
|
+ history = [m.to_llm_dict() for m in main_path_messages]
|
|
|
|
|
+ head_seq = side_branch_ctx.start_head_seq
|
|
|
|
|
+
|
|
|
|
|
+ # 清理
|
|
|
|
|
+ trace.context.pop("active_side_branch", None)
|
|
|
|
|
+ if not config.force_side_branch or len(config.force_side_branch) == 0:
|
|
|
|
|
+ config.force_side_branch = None
|
|
|
|
|
+ logger.info("知识评估完成,队列为空")
|
|
|
|
|
+ if self.trace_store:
|
|
|
|
|
+ await self.trace_store.update_trace(
|
|
|
|
|
+ trace_id, context=trace.context, head_sequence=head_seq,
|
|
|
|
|
+ )
|
|
|
|
|
+ side_branch_ctx = None
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
# 处理工具调用
|
|
# 处理工具调用
|
|
|
# 截断兜底:finish_reason == "length" 说明响应被 max_tokens 截断,
|
|
# 截断兜底:finish_reason == "length" 说明响应被 max_tokens 截断,
|
|
|
# tool call 参数很可能不完整,不应执行,改为提示模型分批操作
|
|
# tool call 参数很可能不完整,不应执行,改为提示模型分批操作
|
|
@@ -1451,6 +1636,13 @@ class AgentRunner:
|
|
|
args_display = args_str[:100] + "..." if len(args_str) > 100 else args_str
|
|
args_display = args_str[:100] + "..." if len(args_str) > 100 else args_str
|
|
|
logger.info(f"[Tool Call] {tool_name}({args_display})")
|
|
logger.info(f"[Tool Call] {tool_name}({args_display})")
|
|
|
|
|
|
|
|
|
|
+ # 获取trigger_event(如果在knowledge_eval侧分支中)
|
|
|
|
|
+ trigger_event_for_tool = None
|
|
|
|
|
+ if side_branch_ctx and side_branch_ctx.type == "knowledge_eval" and self.trace_store:
|
|
|
|
|
+ current_trace = await self.trace_store.get_trace(trace_id)
|
|
|
|
|
+ if current_trace:
|
|
|
|
|
+ trigger_event_for_tool = current_trace.context.get("active_side_branch", {}).get("trigger_event", "unknown")
|
|
|
|
|
+
|
|
|
tool_result = await self.tools.execute(
|
|
tool_result = await self.tools.execute(
|
|
|
tool_name,
|
|
tool_name,
|
|
|
tool_args,
|
|
tool_args,
|
|
@@ -1462,12 +1654,14 @@ class AgentRunner:
|
|
|
"runner": self,
|
|
"runner": self,
|
|
|
"goal_tree": goal_tree,
|
|
"goal_tree": goal_tree,
|
|
|
"knowledge_config": config.knowledge,
|
|
"knowledge_config": config.knowledge,
|
|
|
|
|
+ "sequence": sequence, # 添加sequence用于知识注入记录
|
|
|
# 新增:侧分支信息
|
|
# 新增:侧分支信息
|
|
|
"side_branch": {
|
|
"side_branch": {
|
|
|
"type": side_branch_ctx.type,
|
|
"type": side_branch_ctx.type,
|
|
|
"branch_id": side_branch_ctx.branch_id,
|
|
"branch_id": side_branch_ctx.branch_id,
|
|
|
"is_side_branch": True,
|
|
"is_side_branch": True,
|
|
|
"max_turns": side_branch_ctx.max_turns,
|
|
"max_turns": side_branch_ctx.max_turns,
|
|
|
|
|
+ "trigger_event": trigger_event_for_tool,
|
|
|
} if side_branch_ctx else None,
|
|
} if side_branch_ctx else None,
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|
|
@@ -1609,7 +1803,20 @@ class AgentRunner:
|
|
|
|
|
|
|
|
# 无工具调用
|
|
# 无工具调用
|
|
|
# 如果在侧分支中,已经在上面处理过了(不会走到这里)
|
|
# 如果在侧分支中,已经在上面处理过了(不会走到这里)
|
|
|
- # 主路径无工具调用 → 任务完成,检查是否需要完成后反思
|
|
|
|
|
|
|
+ # 主路径无工具调用 → 任务完成,检查是否需要完成后反思或知识评估
|
|
|
|
|
+
|
|
|
|
|
+ # 检查是否有待评估的知识
|
|
|
|
|
+ if not side_branch_ctx and self.trace_store:
|
|
|
|
|
+ pending = await self.trace_store.get_pending_knowledge_entries(trace_id)
|
|
|
|
|
+ if pending:
|
|
|
|
|
+ logger.info(f"任务即将结束,但仍有 {len(pending)} 条知识未评估,强制触发评估")
|
|
|
|
|
+ config.force_side_branch = ["knowledge_eval"]
|
|
|
|
|
+ trace = await self.trace_store.get_trace(trace_id)
|
|
|
|
|
+ if trace:
|
|
|
|
|
+ trace.context["knowledge_eval_trigger"] = "task_completion"
|
|
|
|
|
+ await self.trace_store.update_trace(trace_id, context=trace.context)
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
if not side_branch_ctx and config.knowledge.enable_completion_extraction and not break_after_side_branch:
|
|
if not side_branch_ctx and config.knowledge.enable_completion_extraction and not break_after_side_branch:
|
|
|
config.force_side_branch = ["reflection"]
|
|
config.force_side_branch = ["reflection"]
|
|
|
break_after_side_branch = True
|
|
break_after_side_branch = True
|