# 最终修复 · 代码级强制工具选择 · 不再依赖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`
>