Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc [SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
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做决定。** |