""" 智能体服务 """ import logging import time from datetime import datetime from typing import Any, Dict, List, Optional from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from ai_platform.models import ( Agent, AgentConversation, AgentMessage, AIWorkflow, LLMModel, ) from .llm_service import LLMService logger = logging.getLogger(__name__) class AgentService: """ 智能体服务 管理智能体的对话和推理 """ def __init__(self, db: Optional[AsyncSession] = None): self._db = db self.llm_service = LLMService(db) def _agent_collaboration_metadata(self, agent: Agent) -> Dict[str, Any]: metadata = { "agent_id": str(agent.id), "agent_code": agent.code, "agent_name": agent.name, "collaboration_role": agent.name or agent.code, "collaboration_mode": agent.mode, } if agent.model_id: metadata["model_id"] = str(agent.model_id) return {key: value for key, value in metadata.items() if value} def _attach_agent_collaboration(self, event: Dict[str, Any], agent: Agent) -> Dict[str, Any]: enriched = dict(event) collaboration = dict(enriched.get("collaboration") or {}) for key, value in self._agent_collaboration_metadata(agent).items(): collaboration.setdefault(key, value) enriched["collaboration"] = collaboration communication = dict(enriched.get("communication") or {}) actor = dict(communication.get("actor") or {}) for key in ( "agent_id", "agent_code", "agent_name", "collaboration_role", "collaboration_mode", "model_id", ): if collaboration.get(key): actor.setdefault(key, collaboration[key]) communication.setdefault("event", enriched.get("type") or "") communication.setdefault("status", "failed" if enriched.get("type") == "error" else "completed") communication.setdefault("channel", "agent_chat") communication.setdefault("actor", actor) communication.setdefault( "message", enriched.get("content") or enriched.get("message") or enriched.get("accumulated_content") or "", ) enriched["communication"] = communication return enriched @staticmethod def _event_to_reasoning_step(event: Dict[str, Any]) -> Optional[Dict[str, Any]]: event_type = event.get('type') if event_type not in { 'action', 'annotation_reply', 'error', 'knowledge_retrieval', 'loop_complete', 'loop_iteration_complete', 'loop_iteration_error', 'loop_iteration_start', 'node_complete', 'node_start', 'observation', 'parallel_complete', 'parallel_start', 'thought', 'tool_call', 'tool_result', 'waiting_input', }: return None collaboration = event.get('collaboration') or {} communication = event.get('communication') or {} config = event.get('waiting_config') or event.get('config') or {} content = ( event.get('node_label') or event.get('content') or communication.get('message') or config.get('title') or config.get('question') or event.get('node_type') or event_type ) step = { 'type': event_type, 'content': content, 'tool': event.get('tool') or '', 'params': event.get('params') or config, 'node_id': event.get('node_id') or '', 'node_type': event.get('node_type') or '', 'branch_id': event.get('branch_id') or collaboration.get('branch_id') or '', 'branch_label': event.get('branch_label') or collaboration.get('branch_label') or '', 'agent_code': collaboration.get('agent_code') or event.get('agent_code') or '', 'agent_name': collaboration.get('agent_name') or event.get('agent_name') or '', 'model': collaboration.get('model') or event.get('model') or '', 'model_id': collaboration.get('model_id') or event.get('model_id') or '', 'subflow_name': collaboration.get('subflow_name') or event.get('subflow_name') or '', 'from_subflow': bool(collaboration.get('from_subflow') or event.get('from_subflow')), 'collaboration_role': collaboration.get('collaboration_role') or '', 'collaboration_mode': collaboration.get('collaboration_mode') or event.get('collaboration_mode') or '', 'communication': communication, 'output': event.get('outputs') or event.get('output'), 'status': 'running' if event_type in {'node_start', 'parallel_start', 'waiting_input', 'loop_iteration_start'} else 'completed', 'timestamp': datetime.now().isoformat(), } return {key: value for key, value in step.items() if value not in (None, '', {}, [])} async def chat( self, agent: Agent, conversation: AgentConversation, user_message: str, application_id: str = None, form_code: str = None, attachments: List[Dict] = None, ): """ 与智能体对话(流式) Args: agent: 智能体 conversation: 对话 user_message: 用户消息 application_id: 子应用 ID(用于表单创建等场景) form_code: 表单编码(从表单列表调用时传入) attachments: 附件列表 [{id, type, name, url, mime_type, size}] Yields: 事件流 """ if agent.mode == 'autonomous': async for event in self._chat_autonomous(agent, conversation, user_message, attachments): yield self._attach_agent_collaboration(event, agent) elif agent.mode == 'dialog_flow': async for event in self._chat_dialog_flow(agent, conversation, user_message, application_id, form_code, attachments): yield self._attach_agent_collaboration(event, agent) else: yield self._attach_agent_collaboration( {'type': 'error', 'content': f'不支持的模式: {agent.mode}'}, agent, ) async def _chat_autonomous( self, agent: Agent, conversation: AgentConversation, user_message: str, attachments: List[Dict] = None, ): """ 自主规划模式对话 优先使用原生 Function Calling,如果模型不支持则回退到 ReAct 模式 Yields: 流式事件 """ # 检查模型是否支持 Function Calling model_id = await self._resolve_agent_model_id(agent) supports_function_call = await self._check_model_supports_function_call(model_id) if supports_function_call: async for event in self._chat_autonomous_function_calling(agent, conversation, user_message, attachments): yield event else: async for event in self._chat_autonomous_react(agent, conversation, user_message, attachments): yield event async def _check_model_supports_function_call(self, model_id) -> bool: """检查模型是否支持 Function Calling""" if not self._db or not model_id: return False result = await self._db.execute( select(LLMModel).where(LLMModel.id == model_id, LLMModel.is_deleted == False) ) model = result.scalar_one_or_none() return model.supports_function_call if model else False async def _resolve_agent_model_id(self, agent: Agent) -> Optional[str]: if not self._db: return str(agent.model_id) if agent.model_id else None if agent.model_id: result = await self._db.execute( select(LLMModel).where( LLMModel.id == agent.model_id, LLMModel.is_deleted == False, LLMModel.is_active == True, ) ) if result.scalar_one_or_none(): return str(agent.model_id) result = await self._db.execute( select(LLMModel) .where( LLMModel.is_deleted == False, LLMModel.is_active == True, LLMModel.model_type == "chat", ) .order_by(LLMModel.sort.desc(), LLMModel.sys_create_datetime.desc()) ) model = result.scalars().first() if not model: return None agent.model_id = model.id await self._db.flush() logger.info( "Agent %s has no active model, fallback to default chat model %s", getattr(agent, "code", agent.id), model.id, ) return str(model.id) async def _chat_autonomous_function_calling( self, agent: Agent, conversation: AgentConversation, user_message: str, attachments: List[Dict] = None, ): """ 自主规划模式对话(支持多模态附件) Yields: 流式事件 """ from ai_platform.providers.base import LLMMessage start_time = time.time() if not self._db: yield {'type': 'error', 'content': '数据库会话未初始化'} return # 创建用户消息(包含附件) # 附件格式: [{file_id, type, name, mime_type, size}] stored_attachments = [] if attachments: for att in attachments: stored_attachments.append({ 'file_id': att.get('file_id'), 'type': att.get('type', 'file'), 'name': att.get('name', ''), 'mime_type': att.get('mime_type', ''), 'size': att.get('size', 0), }) user_msg = AgentMessage( conversation_id=conversation.id, role='user', content=user_message, attachments=stored_attachments, status='completed', ) self._db.add(user_msg) await self._db.flush() # 创建助手消息(pending 状态) assistant_msg = AgentMessage( conversation_id=conversation.id, role='assistant', content='', status='pending', ) self._db.add(assistant_msg) await self._db.flush() reasoning_steps: List[Dict[str, Any]] = [] yield self._attach_agent_collaboration({ 'type': 'start', 'conversation_id': str(conversation.id), 'message_id': str(assistant_msg.id), }, agent) try: # 构建系统提示词 system_prompt = self._build_system_prompt_simple(agent) model_id = await self._resolve_agent_model_id(agent) if not model_id: raise ValueError('未找到可用的 chat 模型,请先在模型配置中启用一个模型') event = self._attach_agent_collaboration({ 'type': 'thought', 'content': '智能体开始分析用户需求并调用默认 chat 模型', 'model_id': model_id, }, agent) step = self._event_to_reasoning_step(event) if step: reasoning_steps.append(step) assistant_msg.reasoning_steps = reasoning_steps.copy() yield event # 标注直接回复检查(高相似度时跳过 LLM,直接返回标注答案) annotation_reply = await self._check_annotation_direct_reply(agent, user_message) if annotation_reply: final_answer = annotation_reply['answer'] elapsed_time = int((time.time() - start_time) * 1000) assistant_msg.content = final_answer assistant_msg.status = 'completed' assistant_msg.elapsed_time = elapsed_time yield { 'type': 'annotation_reply', 'content': f"标注直接回复 (相似度: {annotation_reply['score']:.2f})", } yield { 'type': 'answer', 'content': final_answer, } conversation.message_count = (conversation.message_count or 0) + 2 if conversation.message_count == 2 and (not conversation.title or conversation.title == "新建对话"): self._generate_title(conversation, user_message) agent.message_count = (agent.message_count or 0) + 2 await self._db.commit() yield { 'type': 'complete', 'message_id': str(assistant_msg.id), 'conversation_id': str(conversation.id), 'tokens_used': 0, 'elapsed_time': elapsed_time, } return # 知识库检索增强(RAG) knowledge_context = await self._retrieve_knowledge_context(agent, user_message) if knowledge_context: system_prompt = self._inject_knowledge_context(system_prompt, knowledge_context) event = { 'type': 'knowledge_retrieval', 'content': knowledge_context['summary'], 'result_count': knowledge_context['result_count'], } step = self._event_to_reasoning_step(event) if step: reasoning_steps.append(step) assistant_msg.reasoning_steps = reasoning_steps.copy() yield event # 获取对话历史 history = await self._get_conversation_history(conversation, limit=10) # 构建当前用户消息内容(支持多模态) user_content = LLMMessage.create_multimodal_content(user_message, attachments) history.append({'role': 'user', 'content': user_content}) # 构建消息 messages = [{'role': 'system', 'content': system_prompt}] + history # 检查是否启用流式输出 enable_streaming = getattr(agent, 'enable_streaming', True) model_id = await self._resolve_agent_model_id(agent) if not model_id: raise ValueError('未找到可用的 chat 模型,请先在模型配置中启用一个模型') total_tokens = 0 final_answer = '' if enable_streaming: # 流式输出 accumulated_content = '' async for chunk in self.llm_service.chat_stream( model_id=model_id, messages=messages, temperature=agent.temperature or 0.7, max_tokens=agent.max_tokens or 2048, ): if chunk.content: accumulated_content += chunk.content yield { 'type': 'llm_chunk', 'content': chunk.content, 'accumulated_content': accumulated_content, 'model_id': model_id, } if chunk.is_finished: total_tokens = chunk.total_tokens final_answer = accumulated_content else: # 非流式输出 response = await self.llm_service.chat_async( model_id=model_id, messages=messages, temperature=agent.temperature or 0.7, max_tokens=agent.max_tokens or 2048, ) total_tokens = response.total_tokens final_answer = response.content yield { 'type': 'answer', 'content': final_answer, 'model_id': model_id, } # 更新助手消息 elapsed_time = int((time.time() - start_time) * 1000) assistant_msg.content = final_answer assistant_msg.status = 'completed' assistant_msg.total_tokens = total_tokens assistant_msg.elapsed_time = elapsed_time assistant_msg.reasoning_steps = reasoning_steps.copy() # 更新对话统计 conversation.message_count = (conversation.message_count or 0) + 2 conversation.total_tokens = (conversation.total_tokens or 0) + total_tokens # 自动生成标题(第一次对话时,且标题为默认值) if conversation.message_count == 2 and (not conversation.title or conversation.title == "新建对话"): self._generate_title(conversation, user_message) # 更新智能体统计 agent.message_count = (agent.message_count or 0) + 2 agent.total_tokens = (agent.total_tokens or 0) + total_tokens await self._db.commit() yield { 'type': 'complete', 'message_id': str(assistant_msg.id), 'conversation_id': str(conversation.id), 'tokens_used': total_tokens, 'elapsed_time': elapsed_time, } except Exception as e: logger.exception(f'Agent chat error: {e}') assistant_msg.status = 'failed' assistant_msg.error_message = str(e) error_step = self._event_to_reasoning_step({'type': 'error', 'content': str(e)}) if error_step: reasoning_steps.append(error_step) assistant_msg.reasoning_steps = reasoning_steps.copy() await self._db.commit() yield { 'type': 'error', 'content': str(e), } async def _chat_autonomous_react( self, agent: Agent, conversation: AgentConversation, user_message: str, attachments: List[Dict] = None, ): """ ReAct 模式(简化版,不使用工具,直接复用 function calling 逻辑) Yields: 流式事件 """ # 直接复用简化后的 function calling 逻辑 async for event in self._chat_autonomous_function_calling(agent, conversation, user_message, attachments): yield event async def _chat_dialog_flow( self, agent: Agent, conversation: AgentConversation, user_message: str, application_id: str = None, form_code: str = None, attachments: List[Dict] = None, ): """ 对话流模式对话 直接复用工作流服务的执行逻辑,透传工作流事件 Args: agent: 智能体 conversation: 对话 user_message: 用户消息 application_id: 子应用 ID(用于表单创建等场景) form_code: 表单编码(从表单列表调用时传入) attachments: 附件列表 Yields: 流式事件 """ start_time = time.time() if not self._db: yield {'type': 'error', 'content': '数据库会话未初始化'} return # 创建用户消息(包含附件) # 附件格式: [{file_id, type, name, mime_type, size}] stored_attachments = [] if attachments: for att in attachments: stored_attachments.append({ 'file_id': att.get('file_id'), 'type': att.get('type', 'file'), 'name': att.get('name', ''), 'mime_type': att.get('mime_type', ''), 'size': att.get('size', 0), }) user_msg = AgentMessage( conversation_id=conversation.id, role='user', content=user_message, attachments=stored_attachments, status='completed', ) self._db.add(user_msg) await self._db.flush() # 创建助手消息 assistant_msg = AgentMessage( conversation_id=conversation.id, role='assistant', content='', status='pending', ) self._db.add(assistant_msg) await self._db.flush() reasoning_steps: List[Dict[str, Any]] = [] yield { 'type': 'start', 'conversation_id': str(conversation.id), 'message_id': str(assistant_msg.id), } try: from ai_platform.services.workflow_service import AIWorkflowService workflow_service = AIWorkflowService(self._db) # 检查是否有进行中的工作流运行 workflow_run_id = None if conversation.extra_data: workflow_run_id = conversation.extra_data.get('workflow_run_id') final_content = '' is_waiting = False total_tokens = 0 current_run_id = workflow_run_id # 获取对话历史(如果启用了记忆功能) conversation_history = [] if agent.enable_memory: conversation_history = await self._get_conversation_history( conversation, limit=agent.memory_window or 10 ) if workflow_run_id: # 恢复工作流执行 - 使用异步方法 # 将附件内容(文件解析内容或图片OCR)整合到 user_input 中 enhanced_user_input = user_message if attachments: attachment_texts = [] for att in attachments: text_content = att.get('text_content', '') if text_content: file_name = att.get('name', 'unknown') attachment_texts.append(f"\n[附件: {file_name}]\n{text_content}") if attachment_texts: enhanced_user_input = user_message + '\n' + '\n'.join(attachment_texts) async for event in workflow_service.resume_workflow_stream_async( run_id=workflow_run_id, user_input=enhanced_user_input, conversation_history=conversation_history, ): event_type = event.get('type') if event_type == 'answer': final_content = event.get('content', '') elif event_type == 'llm_chunk': accumulated = event.get('accumulated_content', '') if accumulated: final_content = accumulated elif event_type == 'node_complete': output = (event.get('outputs') or {}).get('output', '') if output: final_content = output elif event_type == 'waiting_input': is_waiting = True elif event_type == 'complete': total_tokens = event.get('total_tokens', 0) output = (event.get('outputs') or {}).get('output', '') if output: final_content = output step = self._event_to_reasoning_step(event) if step: existing_index = next( ( index for index, item in enumerate(reasoning_steps) if item.get('node_id') and item.get('node_id') == step.get('node_id') and item.get('type') == 'node_start' and step.get('type') == 'node_complete' ), -1, ) if existing_index >= 0: reasoning_steps[existing_index] = step else: reasoning_steps.append(step) assistant_msg.reasoning_steps = reasoning_steps.copy() yield event else: # 新对话,启动工作流 - 使用异步方法 if not agent.workflow_id: yield {'type': 'error', 'content': '智能体未配置工作流'} return # 构建工作流输入变量 # 将附件内容(文件解析内容或图片OCR)整合到 user_input 中 enhanced_user_input = user_message if attachments: attachment_texts = [] for att in attachments: text_content = att.get('text_content', '') if text_content: file_name = att.get('name', 'unknown') attachment_texts.append(f"\n[附件: {file_name}]\n{text_content}") if attachment_texts: enhanced_user_input = user_message + '\n' + '\n'.join(attachment_texts) workflow_inputs = await self._build_workflow_inputs(agent, enhanced_user_input) if application_id: workflow_inputs['application_id'] = application_id if form_code: workflow_inputs['form_code'] = form_code # 调试日志 import logging logger = logging.getLogger(__name__) logger.info(f"[AgentService] form_code parameter: {form_code}") logger.info(f"[AgentService] workflow_inputs: {workflow_inputs}") async for event in workflow_service.run_workflow_stream_async( workflow_id=str(agent.workflow_id), inputs=workflow_inputs, conversation_id=str(conversation.id), conversation_history=conversation_history, trigger_type='agent', ): event_type = event.get('type') if event_type == 'start': current_run_id = event.get('run_id') elif event_type == 'answer': final_content = event.get('content', '') elif event_type == 'llm_chunk': accumulated = event.get('accumulated_content', '') if accumulated: final_content = accumulated elif event_type == 'node_complete': output = (event.get('outputs') or {}).get('output', '') if output: final_content = output elif event_type == 'waiting_input': is_waiting = True # 保存工作流运行 ID 到对话元数据 if current_run_id: conversation.extra_data = conversation.extra_data or {} conversation.extra_data['workflow_run_id'] = current_run_id elif event_type == 'complete': total_tokens = event.get('total_tokens', 0) output = (event.get('outputs') or {}).get('output', '') if output: final_content = output step = self._event_to_reasoning_step(event) if step: existing_index = next( ( index for index, item in enumerate(reasoning_steps) if item.get('node_id') and item.get('node_id') == step.get('node_id') and item.get('type') == 'node_start' and step.get('type') == 'node_complete' ), -1, ) if existing_index >= 0: reasoning_steps[existing_index] = step else: reasoning_steps.append(step) assistant_msg.reasoning_steps = reasoning_steps.copy() yield event # 更新助手消息 elapsed_time = int((time.time() - start_time) * 1000) if final_content: assistant_msg.content = final_content assistant_msg.status = 'completed' assistant_msg.elapsed_time = elapsed_time assistant_msg.total_tokens = total_tokens assistant_msg.reasoning_steps = reasoning_steps.copy() # 更新对话统计 conversation.message_count = (conversation.message_count or 0) + 2 conversation.total_tokens = (conversation.total_tokens or 0) + total_tokens # 自动生成标题(第一次对话时,且标题为默认值) if conversation.message_count == 2 and (not conversation.title or conversation.title == "新建对话"): self._generate_title(conversation, user_message) # 如果工作流完成(非等待状态),清除对话元数据 if not is_waiting: if conversation.extra_data: conversation.extra_data.pop('workflow_run_id', None) await self._db.commit() yield { 'type': 'complete', 'elapsed_time': elapsed_time, 'tokens_used': total_tokens, 'total_tokens': total_tokens, 'conversation_id': str(conversation.id), 'message_id': str(assistant_msg.id), } except Exception as e: logger.exception(f'Dialog flow error: {e}') assistant_msg.status = 'failed' assistant_msg.error_message = str(e) error_step = self._event_to_reasoning_step({'type': 'error', 'content': str(e)}) if error_step: reasoning_steps.append(error_step) assistant_msg.reasoning_steps = reasoning_steps.copy() await self._db.commit() yield { 'type': 'error', 'content': str(e), } async def _build_workflow_inputs(self, agent: Agent, user_input: str) -> Dict[str, Any]: workflow_inputs: Dict[str, Any] = {'user_input': user_input} if not self._db or not agent.workflow_id: return workflow_inputs result = await self._db.execute( select(AIWorkflow).where( AIWorkflow.id == agent.workflow_id, AIWorkflow.is_deleted == False, ) ) workflow = result.scalar_one_or_none() if not workflow: return workflow_inputs input_variables = workflow.input_variables or [] if any((item or {}).get('name') == 'request' for item in input_variables): workflow_inputs.setdefault('request', user_input) if any((item or {}).get('name') == 'issue_id' for item in input_variables): workflow_inputs.setdefault('issue_id', self._extract_issue_id(user_input)) return workflow_inputs @staticmethod def _extract_issue_id(text: str) -> str: import re match = re.search(r'\b[A-Z][A-Z0-9]+-\d+\b', text or '', re.IGNORECASE) return match.group(0).upper() if match else 'MANUAL-1' def _build_system_prompt_simple(self, agent: Agent) -> str: """构建简化的系统提示词""" # 基础提示词 base_prompt = agent.system_prompt # 如果没有系统提示词,从人设生成 if not base_prompt and agent.persona: base_prompt = self._generate_prompt_from_persona(agent.persona) if not base_prompt: base_prompt = "你是一个智能助手,可以帮助用户完成各种任务。回答要简洁明了,使用中文。" return base_prompt def _generate_prompt_from_persona(self, persona: Dict[str, Any]) -> str: """从人设配置生成提示词""" parts = [] if persona.get('role'): parts.append(persona['role']) if persona.get('personality'): personalities = persona['personality'] if isinstance(personalities, list): parts.append(f"你的性格特点是:{', '.join(personalities)}。") if persona.get('skills'): skills = persona['skills'] if isinstance(skills, list): parts.append(f"你擅长:{', '.join(skills)}。") if persona.get('background'): parts.append(persona['background']) if persona.get('constraints'): constraints = persona['constraints'] if isinstance(constraints, list): parts.append("注意事项:\n" + "\n".join(f"- {c}" for c in constraints)) return "\n\n".join(parts) async def _get_conversation_history( self, conversation: AgentConversation, limit: int = 10, ) -> List[Dict[str, str]]: """获取对话历史""" if not self._db: return [] result = await self._db.execute( select(AgentMessage).where( AgentMessage.conversation_id == conversation.id, AgentMessage.is_deleted == False, AgentMessage.role.in_(['user', 'assistant']), AgentMessage.status == 'completed' ).order_by(AgentMessage.sys_create_datetime.desc()).limit(limit) ) messages = result.scalars().all() history = [] for msg in reversed(list(messages)): history.append({ 'role': msg.role, 'content': msg.content, }) return history def _generate_title(self, conversation, first_message: str): """自动生成对话标题""" # 简单截取前 20 个字符作为标题 title = first_message[:20] if len(first_message) > 20: title += '...' conversation.title = title async def _check_annotation_direct_reply( self, agent: Agent, query: str, ) -> Optional[Dict[str, Any]]: """ 检查标注直接回复(Dify 风格) 当启用标注回复且匹配到高相似度标注时,直接返回标注答案,跳过 LLM 调用。 Returns: {'answer': str, 'score': float, 'question': str} 或 None """ knowledge_base_ids = getattr(agent, 'knowledge_base_ids', None) or [] if not knowledge_base_ids or not self._db: return None knowledge_config = getattr(agent, 'knowledge_config', None) or {} annotation_reply_enabled = knowledge_config.get('annotation_reply_enabled', False) if not annotation_reply_enabled: return None annotation_threshold = knowledge_config.get('annotation_threshold', 0.9) try: from ai_platform.knowledge.services.retrieval_service import RetrievalService from ai_platform.knowledge.models import KnowledgeBase from sqlalchemy import select # 获取第一个知识库的 embedding 配置 kb_result = await self._db.execute( select(KnowledgeBase).where( KnowledgeBase.id.in_(knowledge_base_ids), KnowledgeBase.is_deleted == False, ) ) first_kb = kb_result.scalars().first() if not first_kb or not first_kb.embedding_model_id: return None service = RetrievalService(self._db) annotation_results = await service._match_annotations( query=query, knowledge_base_ids=knowledge_base_ids, embedding_model_id=first_kb.embedding_model_id, score_threshold=annotation_threshold, dimensions=first_kb.embedding_dimensions, max_results=1, ) if annotation_results: best = annotation_results[0] metadata = getattr(best, 'metadata', {}) or {} logger.info( f'标注直接回复命中: score={best.score}, ' f'question={metadata.get("question", "")}, answer={best.content[:50]}' ) # 记录检索日志 try: from ai_platform.knowledge.models import KnowledgeRetrievalLog log = KnowledgeRetrievalLog( query=query, knowledge_base_ids=knowledge_base_ids, retrieval_mode='annotation', top_k=1, score_threshold=annotation_threshold, result_count=1, results=[{'segment_id': best.segment_id, 'score': best.score, 'kb_id': best.knowledge_base_id}], rerank_applied='false', elapsed_time=0, source='agent_annotation', ) self._db.add(log) await self._db.flush() except Exception as e: logger.warning(f'记录标注回复日志失败: {e}') return { 'answer': best.content, 'score': best.score, 'question': metadata.get('question', ''), } except Exception as e: logger.warning(f'标注直接回复检查失败: {e}') return None async def _retrieve_knowledge_context( self, agent: Agent, query: str, ) -> Optional[Dict[str, Any]]: """ 检索知识库上下文(RAG) Args: agent: 智能体(包含 knowledge_base_ids 和 knowledge_config) query: 用户查询文本 Returns: 知识库上下文字典,无结果时返回 None """ knowledge_base_ids = getattr(agent, 'knowledge_base_ids', None) or [] if not knowledge_base_ids or not self._db: logger.info(f'RAG 跳过: knowledge_base_ids={knowledge_base_ids}, db={bool(self._db)}') return None knowledge_config = getattr(agent, 'knowledge_config', None) or {} logger.info(f'RAG 开始: kb_ids={knowledge_base_ids}, config={knowledge_config}, query={query[:100]}') top_k = knowledge_config.get('top_k', 5) score_threshold = knowledge_config.get('score_threshold', 0.5) retrieval_mode = knowledge_config.get('retrieval_mode', None) rerank_enabled = knowledge_config.get('rerank_enabled', None) rerank_model_id = knowledge_config.get('rerank_model_id', None) try: from ai_platform.knowledge.services.retrieval_service import RetrievalService start_time = time.time() service = RetrievalService(self._db) results = await service.retrieve( query=query, knowledge_base_ids=knowledge_base_ids, top_k=top_k, score_threshold=score_threshold, retrieval_mode=retrieval_mode, rerank_enabled=rerank_enabled, rerank_model_id=rerank_model_id, ) elapsed = int((time.time() - start_time) * 1000) logger.info(f'RAG 检索结果: {len(results)} 条') for r in results: logger.info(f' - segment_id={r.segment_id}, score={r.score}, doc={getattr(r, "document_name", "")}, type={getattr(r, "metadata", {}).get("type", "segment")}') # 记录检索日志(与召回测试共用同一张日志表) try: from ai_platform.knowledge.models import KnowledgeRetrievalLog log = KnowledgeRetrievalLog( query=query, knowledge_base_ids=knowledge_base_ids, retrieval_mode=retrieval_mode or 'hybrid', top_k=top_k, score_threshold=score_threshold, result_count=len(results), results=[ {'segment_id': r.segment_id, 'score': r.score, 'kb_id': r.knowledge_base_id} for r in results ], rerank_applied='true' if rerank_enabled else 'false', elapsed_time=elapsed, source='agent', ) self._db.add(log) await self._db.flush() except Exception as e: logger.warning(f'记录 Agent 检索日志失败: {e}') if not results: return None # 构建上下文文本 context_parts = [] for i, r in enumerate(results, 1): score = getattr(r, 'score', 0) content = getattr(r, 'content', '') metadata = getattr(r, 'metadata', {}) or {} if metadata.get('type') == 'annotation': # 标注类型:显示为 Q&A 对,让 LLM 明确知道这是预设的标准答案 question = metadata.get('question', '') context_parts.append( f"[{i}] [标准问答] (相似度: {score:.2f})\n" f"问题: {question}\n" f"标准答案: {content}" ) else: source = getattr(r, 'document_name', '') or '' context_parts.append( f"[{i}] (来源: {source}, 相似度: {score:.2f})\n{content}" ) context_text = '\n\n'.join(context_parts) summary = f"检索到 {len(results)} 条相关结果" return { 'context': context_text, 'result_count': len(results), 'summary': summary, 'results': [ { 'segment_id': getattr(r, 'segment_id', ''), 'document_name': getattr(r, 'document_name', ''), 'score': getattr(r, 'score', 0), } for r in results ], } except Exception as e: logger.warning(f'知识库检索失败: {e}') return None @staticmethod def _inject_knowledge_context( system_prompt: str, knowledge_context: Dict[str, Any], ) -> str: """ 将知识库检索结果注入系统提示词 参考 Dify 的 RAG 注入方式:在 system prompt 末尾追加参考资料段落, 并指示 LLM 基于资料回答,资料中无相关信息时如实告知。 """ context_text = knowledge_context.get('context', '') if not context_text: return system_prompt rag_instruction = ( "\n\n" "# 参考资料\n" "以下是从知识库中检索到的与用户问题相关的参考资料。\n" "- 标记为[标准问答]的条目是预设的权威问答对,当用户问题与其匹配时,请直接使用标准答案回答,不要自行发挥。\n" "- 其他条目为文档参考内容,请基于这些资料回答用户的问题。\n" "- 如果参考资料中没有相关信息,请基于你自身的知识回答,并说明该回答未基于知识库。\n\n" f"{context_text}" ) return system_prompt + rag_instruction