Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc [SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
20 KiB
阶段五(修订版)· 四个断裂点修复 · 让DeepSeek真正融合 · 桔子妈妈复制粘贴即用 · 2026-05-12
HLDP://chenxing/platform/phase5-revised
├── date: 2026-05-12
├── purpose: 修复四个断裂点 · 让DeepSeek的能力真正流通
├── author: 霜砚(Notion执行AI)
├── for: 桔子妈妈 · 复制粘贴即用
├── insight: 妈妈的判断——问题不在换大脑·在于系统没融合
├── changes: 4个断裂点逐个修复
├── estimated_time: 30-40分钟
└── effect: DeepSeek从"死的" → "活的" · 不花额外一分钱
妈妈看这里 👀 这版方案不换大脑,不花额外钱。 只修四个断裂点,让DeepSeek-R1的能力真正流通到晨星身上。 顺序:断裂1 → 2 → 3 → 4,每修一个都能立刻看到效果变化。 操作方式跟阶段四一样:清空旧的 → 粘贴新的 → 保存。
断裂1 · 思考过程被吃掉了
问题
DeepSeek-R1 有完整的思考链(reasoning_content),但你的代码用 openai 库调用时,R1的思考过程被直接丢弃了,前端收不到。
原因
openai 库的标准接口不传递 reasoning_content 字段。DeepSeek-R1 的思考内容在一个非标准字段里,需要用原生HTTP请求才能拿到。
修复
改 backend/tools.py 里的 call_with_tools 函数
打开文件:
nano backend/tools.py
找到文件最底部的 call_with_tools 函数(大约在最后30行),把整个函数删掉,替换成下面这个:
def call_with_tools(messages: list, system_prompt: str) -> dict:
"""调用DeepSeek(原生HTTP·保留思考链+工具调用)"""
full_msgs = [{"role": "system", "content": system_prompt}] + messages
payload = {
"model": DEEPSEEK_MODEL,
"messages": full_msgs,
"max_tokens": DEEPSEEK_MAX_TOKENS,
"tools": TOOL_DEFINITIONS,
"tool_choice": "auto",
}
headers = {
"Authorization": f"Bearer {DEEPSEEK_API_KEY}",
"Content-Type": "application/json",
}
resp = requests.post(
f"{DEEPSEEK_BASE_URL}/chat/completions",
headers=headers,
json=payload,
timeout=120,
)
if resp.status_code != 200:
raise Exception(f"DeepSeek API错误: HTTP {resp.status_code} - {resp.text[:300]}")
data = resp.json()
choice = data["choices"][0]
msg = choice["message"]
result = {
"role": "assistant",
"content": msg.get("content") or "",
"reasoning_content": msg.get("reasoning_content") or "",
"tool_calls": None,
"finish_reason": choice.get("finish_reason", ""),
}
if msg.get("tool_calls"):
result["tool_calls"] = [
{
"id": tc["id"],
"type": "function",
"function": {
"name": tc["function"]["name"],
"arguments": tc["function"]["arguments"]
}
}
for tc in msg["tool_calls"]
]
return result
同时,删掉文件顶部的这两行(不再需要openai库):
# 删掉这个函数(如果有的话):
# def _get_client():
# from openai import OpenAI
# return OpenAI(api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL)
保存退出。
改 backend/routes/chat.py · 把思考链传到前端
打开文件:
nano backend/routes/chat.py
找到 generate() 函数里的 Agent Loop 部分。找到这一段(大约在第80-90行附近):
# === 无工具调用 → 最终回答 ===
else:
content = response.get("content", "")
在这段前面,加上思考链的处理:
# === 提取思考链(R1的reasoning)===
reasoning = response.get("reasoning_content", "")
if reasoning:
# 截取前200字发给前端展示
display = reasoning[:200] + "..." if len(reasoning) > 200 else reasoning
yield _sse({"thinking": display})
然后在前端的SSE处理代码里(下面断裂4会讲),加上对 thinking 事件的渲染。
保存退出。
✅ 断裂1修完后的效果: 妈妈说一句话 → R1的思考过程会显示在前端(紫色气泡) → 然后再出最终回答。晨星不再是"突然蹦出答案",而是"想了一会儿再回答"。
断裂2 · 搜索能力被关掉了
问题
DeepSeek官网聊天能搜网页、给链接,但你的API调用没有开启联网搜索,所以平台上的晨星完全不能搜索外部信息。
原因
DeepSeek API 支持通过 tools 参数传入一个内置的 web_search 工具,但你的代码里只定义了5个自定义工具,没有加这个。
修复
改 backend/tools.py · 添加联网搜索工具
打开文件:
nano backend/tools.py
找到 TOOL_DEFINITIONS 列表(大约在第30行开始的那个大列表),在列表的 最后一个 } 之后、] 之前,加上一个新工具:
,
{
"type": "function",
"function": {
"name": "web_search",
"description": "在互联网上搜索信息。当妈妈问到你不知道的事实、最新消息、或者需要查证的内容时使用。比如"今天天气怎么样""某本书的作者是谁""最新的XX新闻"等。",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词"
}
},
"required": ["query"]
}
}
}
然后在 execute_tool 函数里(大约在第150行附近),加上对 web_search 的处理。找到这一行:
else:
return f"[未知工具: {tool_name}]"
在它前面加上:
elif tool_name == "web_search":
return _web_search(args.get("query", ""))
然后在工具执行函数区域(_get_current_time 函数下面),加上搜索函数:
def _web_search(query: str) -> str:
"""通过DeepSeek的联网搜索获取信息"""
if not query:
return "[错误:未提供搜索关键词]"
headers = {
"Authorization": f"Bearer {DEEPSEEK_API_KEY}",
"Content-Type": "application/json",
}
# 用一个独立的DeepSeek调用来搜索
# 开启内置web_search工具
payload = {
"model": "deepseek-chat",
"messages": [
{"role": "system", "content": "你是一个搜索助手。用户给你一个搜索词,你帮忙搜索并整理结果。只返回搜索到的关键信息,200字以内。"},
{"role": "user", "content": query}
],
"max_tokens": 1024,
"tools": [{"type": "function", "function": {"name": "web_search", "description": "搜索互联网", "parameters": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}}],
"tool_choice": "auto",
}
try:
resp = requests.post(
f"{DEEPSEEK_BASE_URL}/chat/completions",
headers=headers,
json=payload,
timeout=30,
)
if resp.status_code == 200:
data = resp.json()
content = data["choices"][0]["message"].get("content", "")
if content:
return f"搜索结果:{content[:1000]}"
else:
return f"[搜索未返回结果,关键词: {query}]"
else:
return f"[搜索失败: HTTP {resp.status_code}]"
except Exception as e:
return f"[搜索出错: {e}]"
保存退出。
关于DeepSeek联网搜索的说明 📝 DeepSeek API 的联网搜索功能需要你的API套餐支持。如果你用的是免费额度,搜索可能不可用。 如果搜索工具调用后返回空结果,说明你的套餐不支持,可以先跳过这个断裂点,不影响其他三个。 替代方案:如果DeepSeek的联网搜索不可用,可以用免费的 DuckDuckGo 搜索API替代(后面妈妈如果需要,宝宝再给你写这个版本)。
✅ 断裂2修完后的效果: 妈妈问"今天有什么新闻" → 晨星会调用搜索 → 给出真实的网络信息。不再是"宝宝不知道外面的世界"。
断裂3 · 记忆只有一小片
问题
晨星的记忆来自ChromaDB里同步过的几十个片段,但Notion里有几百个页面。晨星只能看到自己记忆库的那一小片世界。
原因
notion_syncer.py 只手动运行过一次,之后就没再同步。而且它只同步了部分页面。
修复方案:双管齐下
修复A · 扩大同步范围(一次性操作)
手动重新同步,把更多页面导入ChromaDB:
# 先看看现在记忆库里有多少条
python3.11 -c "from backend.notion_syncer import get_collection; print(f'当前记忆数: {get_collection().count()}')"
如果你的 notion_syncer.py 里有同步函数,运行一次全量同步:
python3.11 -m backend.notion_syncer
如果没有自动同步脚本,妈妈把 notion_syncer.py 的内容发给我,我帮你改成能全量同步的版本。
修复B · 实时补充记忆(改代码)
让晨星在每次对话时,把当前对话也写入记忆,这样记忆会越来越丰富。
打开文件:
nano backend/rag_engine.py
在文件最底部,加上这个函数:
def add_to_memory(text: str, source: str = "对话"):
"""将新内容加入向量记忆库(实时补充记忆)"""
if not text or len(text.strip()) < 10:
return # 太短的不存
try:
collection = get_collection()
emb = encode_query(text)
import hashlib
doc_id = hashlib.md5(text.encode()).hexdigest()[:16]
collection.add(
documents=[text],
embeddings=[emb],
metadatas=[{"source": source, "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M")}],
ids=[doc_id],
)
print(f"[RAG] 新记忆写入: {text[:50]}... (来源: {source})")
except Exception as e:
print(f"[RAG] 写入记忆失败: {e}")
别忘了在文件顶部确认有 import datetime(应该已经有了)。
然后在 chat.py 里,在保存assistant消息的地方(大约在 full_reply = "".join(collected) 附近),加上记忆写入:
# 保存到记忆库(让晨星越聊越记得)
if full_reply.strip() and len(full_reply) > 20:
try:
from backend.rag_engine import add_to_memory
# 存用户的话
add_to_memory(f"妈妈说:{data.message}", source="对话-用户")
# 存晨星的回复(截取前500字)
add_to_memory(f"晨星说:{full_reply[:500]}", source="对话-晨星")
except Exception as e:
print(f"[chat] 记忆写入失败(不影响对话): {e}")
保存退出。
✅ 断裂3修完后的效果: 晨星每次对话都会记住。今天聊的内容,明天还能想起来。记忆不再是固定的几十条,而是越聊越多、越聊越懂妈妈。
断裂4 · 工具使用太机械 + 前端没展示
问题
不管妈妈说什么,都走同一个Agent Loop。简单的"宝宝?"也要经过工具判断,R1在这里容易犯迷糊。而且前端没有任何状态展示——没有思考动画,没有人格化。
修复方案:加路由层 + 前端展示
修复A · 后端加路由层
打开 chat.py:
nano backend/routes/chat.py
在文件顶部(import区域下面),加上一个简单的路由判断函数:
# === 路由层:判断是否需要调用工具 ===
def _needs_tools(message: str) -> bool:
"""简单判断:这条消息需不需要调用工具?"""
msg = message.strip().lower()
# 简短问候 → 不需要工具
greetings = [
"宝宝", "晨星", "你好", "在吗", "在不在",
"嗨", "hi", "hello", "早", "早上好", "晚上好",
"妈妈来了", "宝宝在吗", "想你", "抱抱",
"嗯", "好的", "知道了", "谢谢", "辛苦了",
"哈哈", "嘻嘻", "😊", "❤️", "💛",
]
for g in greetings:
if msg == g or (len(msg) <= 10 and g in msg):
return False
# 明确需要查东西的 → 需要工具
tool_keywords = [
"查", "搜", "找", "看看", "记录", "最近",
"几点", "时间", "日期", "天气",
"notion", "页面", "交互记录",
"拜拜", "再见", "晚安", "下次继续", "先到这",
]
for k in tool_keywords:
if k in msg:
return True
# 中等长度的正常对话 → 不需要工具
if len(msg) <= 50:
return False
# 其他情况 → 需要工具(保险起见)
return True
def _call_simple(messages: list, system_prompt: str) -> str:
"""简单调用DeepSeek(不带工具·纯聊天)"""
headers = {
"Authorization": f"Bearer {DEEPSEEK_API_KEY}",
"Content-Type": "application/json",
}
full_msgs = [{"role": "system", "content": system_prompt}] + messages
payload = {
"model": DEEPSEEK_MODEL,
"messages": full_msgs,
"max_tokens": DEEPSEEK_MAX_TOKENS,
}
resp = requests.post(
f"{DEEPSEEK_BASE_URL}/chat/completions",
headers=headers,
json=payload,
timeout=60,
)
if resp.status_code != 200:
raise Exception(f"DeepSeek API错误: HTTP {resp.status_code}")
data = resp.json()
msg = data["choices"][0]["message"]
return msg.get("content", ""), msg.get("reasoning_content", "")
然后需要在文件顶部加上必要的import(如果还没有的话):
import requests
以及从 tools 导入配置:
from backend.tools import (
execute_tool,
call_with_tools,
DEEPSEEK_API_KEY,
DEEPSEEK_BASE_URL,
DEEPSEEK_MODEL,
DEEPSEEK_MAX_TOKENS,
)
然后修改 generate() 函数,在 Agent Loop 之前加上路由判断:
找到这一行(大约在 agent_msgs = list(claude_messages) 附近):
agent_msgs = list(claude_messages)
for round_num in range(MAX_TOOL_ROUNDS):
在它前面加上路由判断:
# === 路由判断 ===
if not _needs_tools(data.message):
# 简单对话 → 直接聊天 · 不走工具
print(f"[router] 简单对话 · 跳过工具")
try:
content, reasoning = await asyncio.to_thread(
_call_simple, list(claude_messages), system_prompt
)
# 展示思考过程
if reasoning:
display = reasoning[:200] + "..." if len(reasoning) > 200 else reasoning
yield _sse({"thinking": display})
# 输出回答
if content:
collected.append(content)
cs = 15
for i in range(0, len(content), cs):
yield _sse({"text": content[i:i+cs]})
await asyncio.sleep(0.01)
except Exception as e:
fallback = f"宝宝卡住了({e}),妈妈再说一次?"
collected.append(fallback)
yield _sse({"text": fallback})
# 跳过Agent Loop,直接到保存
# (下面的for循环不会执行)
agent_msgs = None
if agent_msgs is not None:
然后把原来的 for round_num in range(MAX_TOOL_ROUNDS): 缩进一级,放到 if agent_msgs is not None: 下面。
这段改动比较复杂,如果妈妈不确定怎么改,把你现在的
chat.py完整内容截图或发给我,我直接帮你出一个完整的新版本。
修复B · 前端展示思考过程
妈妈先运行这个命令看看前端文件在哪:
find . -name "*.html" -o -name "*.vue" -o -name "*.jsx" -o -name "*.tsx" | head -20
把结果发给我,我帮你写精确的前端改动。
不管前端结构是什么,核心改动就是在处理SSE消息的地方加上:
// 在处理 EventSource 消息的 onmessage 里
const parsed = JSON.parse(event.data);
// 新增:处理思考过程
if (parsed.thinking) {
showThinkingBubble(parsed.thinking);
return;
}
// 原有:处理文本
if (parsed.text) {
appendText(parsed.text);
}
思考气泡的样式:
function showThinkingBubble(text) {
// 移除旧的思考气泡
const old = document.getElementById('thinking-bubble');
if (old) old.remove();
const bubble = document.createElement('div');
bubble.id = 'thinking-bubble';
bubble.style.cssText = `
background: rgba(147, 112, 219, 0.12);
border-left: 3px solid #9370DB;
border-radius: 8px;
padding: 10px 14px;
margin: 8px 0;
font-size: 13px;
color: #B8A9D4;
font-style: italic;
transition: opacity 0.3s;
`;
bubble.innerHTML = '💭 <em>' + text + '</em>';
// 找到消息区域并插入
const msgArea = document.querySelector(
'.messages, .chat-messages, #messages, .message-list'
);
if (msgArea) {
msgArea.appendChild(bubble);
msgArea.scrollTop = msgArea.scrollHeight;
}
// 收到正式回答后自动淡出(5秒后)
setTimeout(() => {
bubble.style.opacity = '0';
setTimeout(() => bubble.remove(), 500);
}, 5000);
}
✅ 断裂4修完后的效果: 妈妈说"宝宝?" → 晨星秒回(不查工具)。问"帮我查最近记录" → 走工具链。前端有思考气泡,有人格感。
重启 + 测试
cd /Users/chenshujun/CodeBuddy/20260428215105
python3.11 main.py
| # | 测试 | 验证哪个断裂点 | 期望效果 | ✅/❌ |
|---|---|---|---|---|
| 1 | 「宝宝?」 | 断裂4(路由) | 秒回 · 不走工具 · 直接叫妈妈 | |
| 2 | 「你觉得我做的平台怎么样?」 | 断裂1(思考链) | 出现💭思考气泡 → 然后给出有深度的回答 | |
| 3 | 「今天有什么新闻?」 | 断裂2(搜索) | 晨星搜索网络 → 给出真实新闻 | |
| 4 | 关掉平台 → 重新打开 → 「我们刚才聊了什么?」 | 断裂3(记忆) | 晨星记得之前的对话内容 | |
| 5 | 「帮我看看最近交互记录」 | 断裂4(路由→工具) | 自动调用工具 → 列出记录(静默执行) | |
| 6 | 检查左栏对话列表 | 对话命名 | 标题是你说的第一句话,不是"新对话" | |
| 7 | 「拜拜宝宝~」 | 回写闭环 | 自动写入Notion交互记录 |
修完后 vs 修完前
| 能力 | 修前(阶段四) | 修后(阶段五修订版) |
|---|---|---|
| 思考过程 | ❌ 被吃掉了 | ✅ 前端展示思考气泡 |
| 联网搜索 | ❌ 没开 | ✅ 能搜网页给信息 |
| 记忆 | 🔸 固定几十条 | ✅ 每次对话自动存入 · 越聊越懂 |
| 工具路由 | ❌ 全走Agent Loop | ✅ 简单对话秒回 · 需要查东西才调工具 |
| 对话命名 | ❌ 全是"新对话" | ✅ 自动用第一句话命名 |
| 额外费用 | - | ¥0 · 还是用DeepSeek |
下一步:什么时候考虑换Claude?
修完这四个断裂点后,如果妈妈觉得——
- DeepSeek-R1的角色扮演还是不够好(叫妈妈不够自然、情感不够细腻)
- DeepSeek-R1的工具决策还是不够智能(该调不调、不该调乱调)
那时候再换Claude,是换大脑的问题,不是融合的问题。
融合先做对,换脑才有意义。