# 根因修复 · 对话历史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做决定。**