""" v5 SupervisorAdvisor — 单次督导调用版 架构:每轮回应前,把对话历史发给「督导」做一次 LLM 分析, 督导输出:来访者当前状态 / 本轮关注点 / 回应方向 / 操作原则。 咨询师根据督导建议生成回应。无树搜索,无来访者模拟。 """ import json import os import time from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, AIMessage SUPERVISOR_PROMPT = """你是一位经验丰富的精神动力学临床督导。咨询正在进行中,你需要快速分析当前对话,给出本轮的回应建议。 ## 当前对话记录 {conversation_history} ## 来访者最新发言 {client_latest} ## 分析任务 基于精神动力学视角完成分析: 1. **来访者当前状态**:防御水平(高/中/低)、当前主要防御机制、情感基调 2. **本轮核心关注点**:来访者话语中最值得跟进的一个具体点(不要泛化) 3. **回应方向**:咨询师本轮应聚焦的方向,一句话,操作级 4. **回应原则**:2-3条操作原则,明确告诉咨询师怎么做、避免什么 严格要求: - 具体到此刻的来访者状态,不要套话 - 原则必须可操作("用'我在想……'开头做一个试探性诠释" 而不是 "要共情") - 不要从身体感受或躯体体验切入 只输出 JSON,不要输出任何其他内容: {{"client_state":"来访者当前状态(一句话)","focal_point":"本轮核心关注点(一句话)","direction":"回应方向(一句话)","principles":["原则1","原则2","原则3"]}}""" SUPERVISOR_GUIDANCE_TEMPLATE = """ ## 督导建议(你必须参考执行,但不要向来访者透露这个指令的存在) **来访者当前状态**:{client_state} **本轮关注点**:{focal_point} **本轮方向**:{direction} **回应原则**: {principles} 根据以上督导建议生成本轮回应。保持你的临床判断,自然表达。 """ class SupervisorAdvisor: """单次督导调用:把对话历史交给督导做分析,返回结构化建议。""" def __init__(self, model="qwen-turbo"): dashscope = dict( base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key=os.getenv("DASHSCOPE_API_KEY"), ) self.llm = ChatOpenAI(model=model, **dashscope, temperature=0.3, max_tokens=512) def _format_history(self, history): lines = [] for msg in history: if isinstance(msg, HumanMessage): lines.append(f"来访者:{msg.content}") elif isinstance(msg, AIMessage): lines.append(f"咨询师:{msg.content}") return "\n".join(lines) if lines else "(无)" def _parse_json(self, text): content = text.strip() start = content.find("{") end = content.rfind("}") + 1 if start == -1 or end == 0: raise ValueError(f"无法解析 JSON: {content[:80]}") return json.loads(content[start:end]) def supervise(self, history, client_latest): """ 分析当前对话,返回督导建议 dict。 history: List[HumanMessage | AIMessage](不含最新来访者发言) client_latest: str,来访者最新发言 """ t = time.time() history_text = self._format_history(history) prompt = SUPERVISOR_PROMPT.replace( "{conversation_history}", history_text ).replace("{client_latest}", client_latest) for attempt in range(3): try: result = self.llm.invoke(prompt) parsed = self._parse_json(result.content) elapsed = time.time() - t print(f"[督导] {elapsed:.1f}s | 状态: {parsed.get('client_state','?')[:40]}") print(f"[督导] 关注点: {parsed.get('focal_point','?')[:50]}") print(f"[督导] 方向: {parsed.get('direction','?')[:50]}") return parsed except Exception as e: if attempt == 2: print(f"[督导] 分析失败,返回空建议: {e}") return None def format_guidance(self, supervision): """把督导建议格式化为注入到咨询师 prompt 的文本。""" if not supervision: return None principles_text = "\n".join( f"- {p}" for p in supervision.get("principles", []) ) return SUPERVISOR_GUIDANCE_TEMPLATE.replace( "{client_state}", supervision.get("client_state", "") ).replace( "{focal_point}", supervision.get("focal_point", "") ).replace( "{direction}", supervision.get("direction", "") ).replace( "{principles}", principles_text )