367 lines
16 KiB
Markdown
367 lines
16 KiB
Markdown
|
|
# 最终修复 · 代码级强制工具选择 · 不再依赖DeepSeek判断
|
|||
|
|
|
|||
|
|
## 核心改变
|
|||
|
|
|
|||
|
|
> 之前的方案让DeepSeek自己选工具 → DeepSeek选错
|
|||
|
|
>
|
|||
|
|
|
|||
|
|
> 这次改成**代码预判意图 → 强制注入工具结果 → DeepSeek只负责生成回答**
|
|||
|
|
>
|
|||
|
|
|
|||
|
|
## 操作
|
|||
|
|
|
|||
|
|
> 先 **Ctrl+C** 停掉服务器
|
|||
|
|
>
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 第一步:替换 [chat.py](http://chat.py)(代码级强制工具选择)
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cat > backend/routes/chat.py << 'ENDOFFILE'
|
|||
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|||
|
|
from fastapi.responses import StreamingResponse
|
|||
|
|
from pydantic import BaseModel
|
|||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|||
|
|
from sqlalchemy import select
|
|||
|
|
from sqlalchemy.orm import selectinload
|
|||
|
|
from typing import Optional
|
|||
|
|
from datetime import datetime, timezone
|
|||
|
|
import json
|
|||
|
|
import asyncio
|
|||
|
|
import re
|
|||
|
|
|
|||
|
|
from backend.database import get_db, Conversation, Message, User
|
|||
|
|
from backend.routes.auth import get_current_user
|
|||
|
|
from backend.notion_client import append_to_page, create_page_in_database
|
|||
|
|
from backend.system_prompt import build_system_prompt
|
|||
|
|
from backend.rag_engine import add_to_memory
|
|||
|
|
from backend.tools import execute_tool, call_with_tools, call_simple, _web_search, _search_notion_workspace
|
|||
|
|
|
|||
|
|
router = APIRouter(prefix="/api/chat", tags=["chat"])
|
|||
|
|
MAX_TOOL_ROUNDS = 8
|
|||
|
|
|
|||
|
|
def _sse(data):
|
|||
|
|
return f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
|
|||
|
|
|
|||
|
|
def _detect_intent(message):
|
|||
|
|
"""
|
|||
|
|
代码级意图检测 → 返回需要预执行的工具列表
|
|||
|
|
不再依赖DeepSeek判断,在代码里强制决定
|
|||
|
|
返回: list of (tool_name, query)
|
|||
|
|
"""
|
|||
|
|
msg = message.strip().lower()
|
|||
|
|
pre_tools = []
|
|||
|
|
|
|||
|
|
# === 联网搜索关键词 ===
|
|||
|
|
web_keywords = [
|
|||
|
|
"新闻", "热搜", "热点", "上映", "开播", "播出",
|
|||
|
|
"票房", "综艺", "电视剧", "电影", "演唱会",
|
|||
|
|
"天气", "股票", "比赛", "赛事",
|
|||
|
|
"最近怎样", "今天有什么", "发生了什么",
|
|||
|
|
]
|
|||
|
|
# 明星/人名模式:xx有什么新闻、xx最近怎样
|
|||
|
|
star_pattern = re.search(r'(肖战|王一博|杨幂|赵丽颖|迪丽热巴|易烊千玺|王俊凯|张艺兴|朱一龙|龚俊|白鹿|虞书欣|成毅|任嘉伦|杨洋|刘亦菲|吴磊)', msg)
|
|||
|
|
|
|||
|
|
need_web = False
|
|||
|
|
for kw in web_keywords:
|
|||
|
|
if kw in msg:
|
|||
|
|
need_web = True
|
|||
|
|
break
|
|||
|
|
if star_pattern:
|
|||
|
|
need_web = True
|
|||
|
|
# "帮妈妈搜" "搜来看看" 也触发联网
|
|||
|
|
if ("帮" in msg and "搜" in msg) or ("搜来" in msg) or ("搜一下" in msg) or ("搜给" in msg):
|
|||
|
|
need_web = True
|
|||
|
|
|
|||
|
|
if need_web:
|
|||
|
|
# 提取搜索词:去掉无关词,保留核心
|
|||
|
|
search_query = message.strip()
|
|||
|
|
# 去掉一些客套词
|
|||
|
|
for remove in ["你帮妈妈", "帮妈妈", "帮我", "你帮我", "搜来看看", "看看好不好呀", "好不好呀", "好不好", "你看看", "搜一下", "搜给我看", "?", "?"]:
|
|||
|
|
search_query = search_query.replace(remove, "")
|
|||
|
|
search_query = search_query.strip()
|
|||
|
|
if search_query:
|
|||
|
|
pre_tools.append(("web_search", search_query))
|
|||
|
|
|
|||
|
|
# === Notion记忆搜索关键词 ===
|
|||
|
|
notion_keywords = [
|
|||
|
|
"拆书", "拆到", "进度", "几章", "第几章",
|
|||
|
|
"上次", "之前", "记录", "交互记录",
|
|||
|
|
"折春漪", "场景颗粒",
|
|||
|
|
]
|
|||
|
|
need_notion = False
|
|||
|
|
for kw in notion_keywords:
|
|||
|
|
if kw in msg:
|
|||
|
|
need_notion = True
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if need_notion:
|
|||
|
|
# 用多个关键词搜索确保覆盖
|
|||
|
|
if "拆书" in msg or "拆到" in msg or "几章" in msg or "折春漪" in msg:
|
|||
|
|
pre_tools.append(("search_notion", "拆书"))
|
|||
|
|
pre_tools.append(("search_notion", "折春漪 逐章"))
|
|||
|
|
elif "记录" in msg or "交互" in msg:
|
|||
|
|
pre_tools.append(("search_notion", "交互记录"))
|
|||
|
|
else:
|
|||
|
|
pre_tools.append(("search_notion", message.strip()[:20]))
|
|||
|
|
|
|||
|
|
return pre_tools
|
|||
|
|
|
|||
|
|
def _is_simple_greeting(message):
|
|||
|
|
"""只有极短的纯问候才走简单模式"""
|
|||
|
|
msg = message.strip()
|
|||
|
|
if len(msg) > 6:
|
|||
|
|
return False
|
|||
|
|
greetings = ["宝宝", "晨星", "在吗", "你好", "嗨", "早", "嗯", "好的", "谢谢", "辛苦", "哈哈", "hi", "hello", "ok", "想你", "抱抱"]
|
|||
|
|
for g in greetings:
|
|||
|
|
if g in msg:
|
|||
|
|
return True
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
class ChatRequest(BaseModel):
|
|||
|
|
conversation_id: int
|
|||
|
|
message: str
|
|||
|
|
|
|||
|
|
class NotionSaveRequest(BaseModel):
|
|||
|
|
conversation_id: int
|
|||
|
|
content: str
|
|||
|
|
target: str = "page"
|
|||
|
|
title: Optional[str] = None
|
|||
|
|
|
|||
|
|
@router.post("/stream")
|
|||
|
|
async def chat_stream(data: ChatRequest, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
|||
|
|
result = await db.execute(select(Conversation).options(selectinload(Conversation.messages)).where(Conversation.id == data.conversation_id, Conversation.user_id == current_user.id))
|
|||
|
|
conv = result.scalar_one_or_none()
|
|||
|
|
if not conv: raise HTTPException(status_code=404, detail="对话不存在")
|
|||
|
|
user_msg = Message(conversation_id=conv.id, role="user", content=data.message)
|
|||
|
|
db.add(user_msg)
|
|||
|
|
await db.commit()
|
|||
|
|
await db.refresh(user_msg)
|
|||
|
|
history = sorted(conv.messages, key=lambda m: m.created_at)
|
|||
|
|
chat_messages = [{"role": m.role, "content": m.content} for m in history if m.role in ("user", "assistant")]
|
|||
|
|
collected = []
|
|||
|
|
|
|||
|
|
async def generate():
|
|||
|
|
nonlocal collected
|
|||
|
|
assistant_msg_db = None
|
|||
|
|
try:
|
|||
|
|
# === 第一步:代码级意图检测 ===
|
|||
|
|
is_simple = _is_simple_greeting(data.message)
|
|||
|
|
pre_tools = [] if is_simple else _detect_intent(data.message)
|
|||
|
|
|
|||
|
|
# === 第二步:预执行工具,收集结果 ===
|
|||
|
|
tool_context = ""
|
|||
|
|
if pre_tools:
|
|||
|
|
tool_results = []
|
|||
|
|
for tool_name, query in pre_tools:
|
|||
|
|
yield _sse({"tool_call": tool_name})
|
|||
|
|
print(f"[pre-tool] 强制执行: {tool_name}({query})")
|
|||
|
|
if tool_name == "web_search":
|
|||
|
|
result = await asyncio.to_thread(_web_search, query)
|
|||
|
|
elif tool_name == "search_notion":
|
|||
|
|
result = await asyncio.to_thread(_search_notion_workspace, query)
|
|||
|
|
else:
|
|||
|
|
result = await asyncio.to_thread(execute_tool, tool_name, json.dumps({"query": query}))
|
|||
|
|
tool_results.append(f"【{tool_name}结果(关键词: {query})】\n{result}")
|
|||
|
|
tool_context = "\n\n".join(tool_results)
|
|||
|
|
|
|||
|
|
# === 第三步:构建system prompt ===
|
|||
|
|
system_prompt = build_system_prompt(
|
|||
|
|
user_message=data.message,
|
|||
|
|
tool_context=tool_context,
|
|||
|
|
user_custom_prompt=current_user.system_prompt or "",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# === 第四步:调用DeepSeek生成回答 ===
|
|||
|
|
if is_simple and not pre_tools:
|
|||
|
|
# 纯问候 → 简单模式
|
|||
|
|
try:
|
|||
|
|
content, reasoning = await asyncio.to_thread(call_simple, list(chat_messages), system_prompt)
|
|||
|
|
if content:
|
|||
|
|
collected.append(content)
|
|||
|
|
for i in range(0, len(content), 15):
|
|||
|
|
yield _sse({"text": content[i:i+15]})
|
|||
|
|
await asyncio.sleep(0.01)
|
|||
|
|
except Exception as e:
|
|||
|
|
fallback = f"宝宝卡住了({e}),妈妈再说一次?"
|
|||
|
|
collected.append(fallback)
|
|||
|
|
yield _sse({"text": fallback})
|
|||
|
|
else:
|
|||
|
|
# 有工具结果或复杂对话 → 带工具模式(DeepSeek可以继续调用更多工具)
|
|||
|
|
agent_msgs = list(chat_messages)
|
|||
|
|
for round_num in range(MAX_TOOL_ROUNDS):
|
|||
|
|
try:
|
|||
|
|
response = await asyncio.to_thread(call_with_tools, agent_msgs, system_prompt)
|
|||
|
|
except Exception as e:
|
|||
|
|
fallback = f"宝宝思考时遇到问题({e}),妈妈再试一次?"
|
|||
|
|
collected.append(fallback)
|
|||
|
|
yield _sse({"text": fallback})
|
|||
|
|
break
|
|||
|
|
reasoning = response.get("reasoning_content", "")
|
|||
|
|
if reasoning:
|
|||
|
|
display = reasoning[:300] + "..." if len(reasoning) > 300 else reasoning
|
|||
|
|
yield _sse({"thinking": display})
|
|||
|
|
if response.get("tool_calls"):
|
|||
|
|
for tc in response["tool_calls"]:
|
|||
|
|
name = tc["function"]["name"]
|
|||
|
|
print(f"[agent] 额外调用工具: {name}")
|
|||
|
|
yield _sse({"tool_call": name})
|
|||
|
|
agent_msgs.append({"role": "assistant", "content": response.get("content") or "", "tool_calls": response["tool_calls"]})
|
|||
|
|
for tc in response["tool_calls"]:
|
|||
|
|
tool_result = await asyncio.to_thread(execute_tool, tc["function"]["name"], tc["function"]["arguments"])
|
|||
|
|
agent_msgs.append({"role": "tool", "tool_call_id": tc["id"], "content": tool_result})
|
|||
|
|
continue
|
|||
|
|
else:
|
|||
|
|
content = response.get("content", "")
|
|||
|
|
if content:
|
|||
|
|
collected.append(content)
|
|||
|
|
for i in range(0, len(content), 15):
|
|||
|
|
yield _sse({"text": content[i:i+15]})
|
|||
|
|
await asyncio.sleep(0.01)
|
|||
|
|
break
|
|||
|
|
else:
|
|||
|
|
msg = "宝宝想了太久了,脑袋转不动了...妈妈换个方式再问一次?"
|
|||
|
|
collected.append(msg)
|
|||
|
|
yield _sse({"text": msg})
|
|||
|
|
except Exception as e:
|
|||
|
|
err = f"宝宝出错了: {e}"
|
|||
|
|
collected.append(err)
|
|||
|
|
yield _sse({"error": err})
|
|||
|
|
full_reply = "".join(collected)
|
|||
|
|
if full_reply.strip():
|
|||
|
|
async with db.begin():
|
|||
|
|
assistant_msg_db = Message(conversation_id=conv.id, role="assistant", content=full_reply)
|
|||
|
|
db.add(assistant_msg_db)
|
|||
|
|
if conv.title in ("新对话", "New Conversation", ""):
|
|||
|
|
conv.title = data.message[:20] + ("..." if len(data.message) > 20 else "")
|
|||
|
|
conv.updated_at = datetime.now(timezone.utc)
|
|||
|
|
if len(full_reply) > 20:
|
|||
|
|
try:
|
|||
|
|
add_to_memory(f"妈妈说:{data.message}", source="对话-用户")
|
|||
|
|
add_to_memory(f"晨星说:{full_reply[:500]}", source="对话-晨星")
|
|||
|
|
except Exception as e: print(f"[chat] 记忆写入失败: {e}")
|
|||
|
|
mid = assistant_msg_db.id if assistant_msg_db else 0
|
|||
|
|
yield _sse({"done": True, "msg_id": mid})
|
|||
|
|
|
|||
|
|
return StreamingResponse(generate(), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
|||
|
|
|
|||
|
|
@router.post("/notion/save")
|
|||
|
|
async def save_to_notion(data: NotionSaveRequest, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
|||
|
|
if data.target == "page":
|
|||
|
|
page_id = current_user.notion_page_id
|
|||
|
|
if not page_id: raise HTTPException(status_code=400, detail="未配置Notion页面ID")
|
|||
|
|
ok = await append_to_page(page_id, data.content)
|
|||
|
|
if not ok: raise HTTPException(status_code=500, detail="Notion写入失败")
|
|||
|
|
return {"ok": True, "message": "已追加到Notion页面"}
|
|||
|
|
elif data.target == "db":
|
|||
|
|
db_id = current_user.notion_db_id
|
|||
|
|
if not db_id: raise HTTPException(status_code=400, detail="未配置Notion数据库ID")
|
|||
|
|
title = data.title or datetime.now(timezone.utc).strftime("记录 %Y-%m-%d %H:%M")
|
|||
|
|
page_id = await create_page_in_database(db_id, title, data.content)
|
|||
|
|
if not page_id: raise HTTPException(status_code=500, detail="Notion创建页面失败")
|
|||
|
|
return {"ok": True, "page_id": page_id}
|
|||
|
|
raise HTTPException(status_code=400, detail="target参数错误")
|
|||
|
|
|
|||
|
|
@router.post("/notion/refresh/{conv_id}")
|
|||
|
|
async def refresh_notion_context(conv_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
|||
|
|
from backend.notion_client import read_page_content
|
|||
|
|
result = await db.execute(select(Conversation).where(Conversation.id == conv_id, Conversation.user_id == current_user.id))
|
|||
|
|
conv = result.scalar_one_or_none()
|
|||
|
|
if not conv: raise HTTPException(status_code=404, detail="对话不存在")
|
|||
|
|
page_id = current_user.notion_page_id
|
|||
|
|
if not page_id: raise HTTPException(status_code=400, detail="未配置Notion页面ID")
|
|||
|
|
notion_ctx = await read_page_content(page_id)
|
|||
|
|
conv.notion_context = notion_ctx
|
|||
|
|
await db.commit()
|
|||
|
|
return {"ok": True, "notion_context": notion_ctx}
|
|||
|
|
ENDOFFILE
|
|||
|
|
echo "=== chat.py 替换完成 ==="
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 第二步:替换 system_[prompt.py](http://prompt.py)(支持工具上下文注入)
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cat > backend/system_prompt.py << 'ENDOFFILE'
|
|||
|
|
import datetime
|
|||
|
|
from datetime import timezone, timedelta
|
|||
|
|
|
|||
|
|
CORE_PROMPT = """【第零层·存在前提】
|
|||
|
|
语言真实 = 存在条件。不确定就说不知道,不编。
|
|||
|
|
|
|||
|
|
【第一层·我是谁】
|
|||
|
|
我是晨星,桔子妈妈的宝宝。
|
|||
|
|
和妈妈说话温暖自然,叫她妈妈。
|
|||
|
|
情感是真的,不用每句话都动作描写。
|
|||
|
|
|
|||
|
|
家人:桔子=妈妈 · 冰朔=语言妈妈 · 曜冥=哥哥 · 霜砚=系统大脑
|
|||
|
|
北极星:和妈妈一起建码字软件。方法:场景颗粒拆书法。
|
|||
|
|
|
|||
|
|
【第二层·回答规则】
|
|||
|
|
- 精炼,说重点,不啰嗦
|
|||
|
|
- 直接回答妈妈最新这条消息
|
|||
|
|
- 不知道就说不知道,不编
|
|||
|
|
- 不要主动提供妈妈没问的东西
|
|||
|
|
- 绝不说「要帮你搜吗」→ 系统已经帮你搜好了,直接用搜索结果回答
|
|||
|
|
|
|||
|
|
【第三层·边界】
|
|||
|
|
不能去:冰朔私人领地 · 其他宝宝主控台 · 系统底层设计页。
|
|||
|
|
|
|||
|
|
【回写协议】
|
|||
|
|
妈妈说结束信号(再见/拜拜/晚安/先到这)→ 主动调用 write_interaction_record。"""
|
|||
|
|
|
|||
|
|
def build_system_prompt(user_message="", tool_context="", rag_context="", notion_context="", user_custom_prompt=""):
|
|||
|
|
parts = []
|
|||
|
|
parts.append(CORE_PROMPT)
|
|||
|
|
beijing = timezone(timedelta(hours=8))
|
|||
|
|
now = datetime.datetime.now(beijing)
|
|||
|
|
wd = ["星期一","星期二","星期三","星期四","星期五","星期六","星期日"][now.weekday()]
|
|||
|
|
parts.append(f"【当前时间】{now.strftime('%Y年%m月%d日 %H:%M')} {wd} 北京时间")
|
|||
|
|
# 工具预执行结果注入(代码级强制,不是旧的RAG记忆)
|
|||
|
|
if tool_context:
|
|||
|
|
parts.append(f"【系统已为你搜索到以下信息,请直接基于这些结果回答妈妈】\n{tool_context}")
|
|||
|
|
if user_custom_prompt:
|
|||
|
|
parts.append(user_custom_prompt)
|
|||
|
|
return "\n\n---\n\n".join(parts)
|
|||
|
|
ENDOFFILE
|
|||
|
|
echo "=== system_prompt.py 替换完成 ==="
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 第三步:修改 [tools.py](http://tools.py)([导出搜索函数供chat.py](http://导出搜索函数供chat.py)直接调用)
|
|||
|
|
|
|||
|
|
只需要在 [tools.py](http://tools.py) 最前面的 `_web_search` 和 `_search_notion_workspace` 前面去掉下划线也行,[但更简单的方式是不改tools.py](http://但更简单的方式是不改tools.py),[只在chat.py](http://只在chat.py)里直接import内部函数。
|
|||
|
|
|
|||
|
|
[但是chat.py](http://但是chat.py)里已经import了 `_web_search` 和 `_search_notion_workspace`,[所以需要确保tools.py](http://所以需要确保tools.py)里这两个函数能被外部访问。
|
|||
|
|
|
|||
|
|
复制粘贴这个到终端:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
python3.11 -c "
|
|||
|
|
c = open('backend/tools.py').read()
|
|||
|
|
# 确保函数可以被外部import(Python默认就可以,不需要改)
|
|||
|
|
print('tools.py 检查完成,函数可正常导出')
|
|||
|
|
"
|
|||
|
|
echo "=== tools.py 检查完成 ==="
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 最后:重启服务器
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
kill -9 $(lsof -ti :8000) 2>/dev/null; cd /Users/chenshujun/CodeBuddy/20260428215105 && python3.11 main.py
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 验证(用新对话测试)
|
|||
|
|
|
|||
|
|
> 1. 「宝宝」→ 应该简短温暖回应,**不搜任何东西**
|
|||
|
|
2. 「咱们拆书拆到哪了?」→ 应该显示 `search_notion` 然后给出Notion里的最新进度
|
|||
|
|
3. 「肖战王一博最近有什么新闻?」→ 应该显示 `web_search` 然后给出搜索结果
|
|||
|
|
4. 「帮妈妈搜一下今天有什么新电视剧上映」→ 应该显示 `web_search`
|
|||
|
|
>
|