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