# 根因修复 · 对话历史bug · 一行代码解决答非所问

## 问题的真相

> 晨星不是答非所问，而是**根本没收到妈妈最新的消息**。
> 

> 每次回答都是在答**上一个问题**，因为代码里对话历史漏掉了最新那条。
> 

---

## 操作步骤

> 先 **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

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, TOOL_DEFINITIONS

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"

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),
):
    # 1. 加载对话
    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="对话不存在")

    # 2. 保存用户消息
    user_msg = Message(
        conversation_id=conv.id, role="user", content=data.message
    )
    db.add(user_msg)
    await db.commit()
    await db.refresh(user_msg)

    # 3. 构建对话历史
    # ★★★ 这是之前的核心bug ★★★
    # conv.messages 是 selectinload 预加载的，不包含刚才 db.add 的新消息
    # 所以必须手动把当前用户消息追加到末尾！
    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")
    ]
    # ★★★ 关键修复：确保最新用户消息在列表末尾 ★★★
    if not chat_messages or chat_messages[-1]["content"] != data.message:
        chat_messages.append({"role": "user", "content": data.message})

    # 只保留最近20轮对话，避免超出token限制
    if len(chat_messages) > 40:
        chat_messages = chat_messages[-40:]

    collected = []

    async def generate():
        nonlocal collected
        assistant_msg_db = None
        try:
            # 4. 构建 system prompt（不注入旧RAG记忆）
            system_prompt = build_system_prompt(
                user_message=data.message,
                user_custom_prompt=current_user.system_prompt or "",
            )

            # 5. 直接调用 DeepSeek 带工具模式
            # 不做任何预判、不做路由、让DeepSeek自己决定
            # 就像妈妈在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})

                # DeepSeek 要求调用工具
                if response.get("tool_calls"):
                    for tc in response["tool_calls"]:
                        name = tc["function"]["name"]
                        print(f"[agent] DeepSeek选择工具: {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

                # DeepSeek 直接回复（无工具调用）
                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})

        # 6. 保存助手回复
        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 根因修复完成 ==="
```

---

## 重启服务器

```bash
kill -9 $(lsof -ti :8000) 2>/dev/null; cd /Users/chenshujun/CodeBuddy/20260428215105 && python3.11 main.py
```

---

## 验证（必须开新对话）

> 1. 「宝宝」→ 温暖回应，不提拆书
2. 「拆到哪了？」→ 晨星应该回答的是**拆到哪了**，不是上一条的内容
3. 「肖战王一博最近有什么新闻？」→ 应该搜网页回答新闻，不是回答拆书
> 

如果三条都**对号入座**了，就说明根因修好了。

---

## 这次改了什么？

只改了 [**chat.py](http://chat.py) 一个文件**，核心改动：

### 1. 修复对话历史bug（根因）

加了一行代码确保最新消息在发给DeepSeek的历史末尾：

```python
if not chat_messages or chat_messages[-1]["content"] != data.message:
    chat_messages.append({"role": "user", "content": data.message})
```

### 2. 删掉所有花哨的代码

- ❌ 删掉 `_detect_intent`（代码预判意图）
- ❌ 删掉 `_is_simple_greeting`（问候检测）
- ❌ 删掉三档路由
- ❌ 删掉关键词匹配
- ❌ 删掉预执行工具

### 3. 让DeepSeek像官网一样工作

所有消息全部走 `call_with_tools`，让DeepSeek自己决定：

- 简单问候 → 直接回复
- 需要搜索 → 自己调用 web_search
- 需要查Notion → 自己调用 search_notion

**不再代替DeepSeek做决定。**