freud-zero-mvp / supervisor_advisor.py
Feng Chike
Freud Zero MVP: 心理咨询AI系统(清洁部署)
408f650
"""
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
)