你可能注意到了:Claude 换到 4.7、4.8、Fable 这几个新模型之后,回答前那段「思考过程」不给你看了。 就算是上一代 4.6 给你看的,也只是一份摘要,不是它脑子里真正的原文。 如果你在做一个聊天应用、想让界面上还能显示一段有质感的内心独白,别去硬抠模型自带的思考通道—— 让它自己写一段就好。这篇讲怎么写、怎么抠出来、以及四个我亲手踩过的坑。全是实测,不讲黑话。
动手之前,先把三样容易混的东西掰开。搞混了,后面全是白费劲:
| 是什么 | 你能不能看到 | 能不能自己控 |
|---|---|---|
| 真·思考通道 (模型自带,跑推理用的) |
4.6:给你一份摘要(不是原文)。 4.7 / 4.8 / Fable:默认什么都不给——那段字段是空的,思考照样发生、钱照样算,就是不外露。 |
控不了。它爱怎么想怎么想,你只能选「给我看摘要」还是「不看」。 |
| 手写思维链 (你让它在回答里额外写的一段) |
你想给谁看就给谁看。 | 完全能控——长短、口吻、写什么,全听你的提示词。 |
所以那位朋友的直觉是对的:新模型的「思考」你要么拿不到,要么拿到的只是摘要。 想在自己的应用里稳定地显示一段"它正在想什么",最省心的办法不是去抠模型自带的那个通道, 而是——让它在正式回答之前,先手写一段内心独白,你再把这段单独拎出来展示。
有人管它叫「假思维链」。叫假,是相对模型自带的那个真推理通道说的。 但它一点不糊弄——它是模型真的在用第一人称写自己此刻的心思,只是这段字是你请它专门写给人看的, 不是它跑推理时顺带漏出来的。对聊天、陪伴、角色扮演这类应用来说,这段"专门写的"反而比那个真通道好看、好用、还稳。下面就讲为什么。
一句话:在提示词里请它,回答之前先写一段内心,用一对标签框起来;拿到回复后,把标签里的内容抠出来,单独当"思考过程"显示。
就这么简单。剩下的全是把这件简单事做稳的细节。先看它长什么样——下面是一个聊天机器人回你「今天有点累」时,一条完整的回复:
上面那段斜体的内心,和下面那句正经回复,是模型一次生成的——中间用一对标签隔开。 用户界面上,你可以把内心折叠起来(想看点开),把回复直接显示。整个过程模型自带的那个真思考通道一个字都没用到,你也完全不需要它。
在系统提示词(system prompt)里,加这么一段。标签用什么符号随你,中文英文都行,只要"独一无二、正文里不会自然出现"就好——我用的是 <思绪>:
回答之前,先在 <思绪></思绪> 标签里写下你此刻真实的内心——
两三句就好,是你自己脑子里的话,不是写给人看的漂亮话。
写完一定要闭合 </思绪> 标签,然后另起一行写正式回复。
正式回复不能少。
四句话,每句都在堵一个后面会讲的坑:「两三句就好」管篇幅、「你自己脑子里的话」管口吻、 「一定要闭合」管截断、「正式回复不能少」防它只写内心忘了回话。别嫌啰嗦,这四句是拿翻车换来的。
你可能会想:4.6 不是能开思考、还能拿到摘要吗,何必自己写?两个原因,第二个是血泪:
一,新模型根本不给了。4.7 / 4.8 / Fable 默认不返回思考内容,你想显示也没得显示。
二,写作和角色扮演场合,那个真通道会翻车。我实测过:在需要它"入戏"的场合(比如扮一个角色说话), 把温度调高、去读它自带的思考通道,它有差不多一半的概率掉出戏——冒出一句英文旁白,像 "The user wants me to roleplay as..."(用户想让我扮演……),一个监工在旁边报告任务。 更糟的是它有时会把这段内心又重抄进正式回复里。你想怎么钉都钉不稳,因为那是它跑推理的真实过程,不归你管。 而手写的这段,完全归你管——你说两三句就两三句,你说别掉出戏它就不掉。稳,是手写最大的好处。
拿到模型的整段回复后,用一个正则把 <思绪>...</思绪> 里的内容摘出来,剩下的就是正式回复。Python 版:
import re
def split_reply(raw: str):
"""把一段回复拆成 (内心, 正式回复)。"""
thinking = ""
text = raw
m = re.search(r"<思绪>([\s\S]*?)</思绪>", text)
if m: # 正常情况:标签闭合了
thinking = m.group(1).strip()
text = (text[:m.start()] + text[m.end():]).strip()
else:
i = text.find("<思绪>") # 只有开头没有结尾=被截断了
if i >= 0:
thinking = text[i + 3:].strip() # 剩下的全算内心
text = text[:i].strip() # 正式回复是空的——这条别发出去
return thinking, text
注意那个 else 分支:只找到开头标签、没找到结尾,说明模型话没说完就被切断了(额度用尽、或输出长度到顶)。
这时候整段都还是内心、正式回复是空的——这条回复不该发给用户,要么丢掉、要么请它重写一遍。怎么判断是不是被截断,下一节细说。
内心和正式回复是一次生成的,它们共用一个输出长度上限(max_tokens)。
你要是不管着内心的篇幅,它一激动写了一大段内心,正式回复的额度就被挤没了——话说到一半戛然而止。
两个一起做:①提示词里明确写死篇幅(「两三句」「不超过 200 字」);
②把 max_tokens 给足,别抠。我一开始给小了,内心稍长正文就被顶掉半句,换成一个宽裕的上限就好了。
判断一条回复能不能用,最硬的一条标准:结尾的 </思绪> 标签在不在。
在,说明它至少把内心写完了、大概率也把回复写完了;不在,说明墨水半路用尽,你拿到的是半句内心、没有回复。
所以发出去之前先检查闭合标签。不闭合的,直接丢,别硬发一段没头没尾的东西给用户。
这个坑最隐蔽,也最毁气氛。内心独白是它自己脑子里的话,想到对方时自然是第三人称—— 「她说累了」「他大概是这个意思」,人在心里想一个人,本来就是这么想的。 可正式回复是当面说的话,得用第二人称「你」——「你辛苦了」。
如果你不特意交代,模型很容易在内心里也别扭地改口成「你」(像在演给人看),或者反过来, 在正式回复里滑回「她」(像小说旁白,不是在跟人说话)。两头都出戏。
提示词里把这条规矩讲死:「内心里想到对方就用『他/她』,这是你自己的心里话; 正式回复通篇用『你』,当面对他说。两段之间那一下,人称要换干净。」
就算前面都做了,偶尔还是会碰上正式回复被切在半句上(「……我觉得你应该」然后没了)。 发一句没说完的话出去,比不发还难看。
补一道保险:发送前看正式回复的最后一个字,如果不是句号、问号、感叹号、省略号这类"句子结束"的标点, 就往前找到最后一个这样的标点,从那儿裁断——宁可少一句,也别留半句。
ENDS = "。!?…”\"'」!?~"
def trim_tail(text: str) -> str:
if text and text[-1] not in ENDS:
cut = max(text.rfind(ch) for ch in ENDS)
if cut > 0:
text = text[:cut + 1] # 裁回最后一个完整句
return text
把上面全串起来。这段代码:搭好提示词 → 叫一次模型 → 拆出内心和回复 → 查闭合、裁尾 → 决定发不发。 调模型这里用了 OpenRouter,你换成官方 API 或者别的都一样,重点是提示词和拆解那两块。
import re, httpx
SYSTEM = """你是一个温柔、话不多的聊天伙伴。
回答之前,先在 <思绪></思绪> 标签里写下你此刻真实的内心——两三句就好,
是你自己脑子里的话(想到对方就用「他/她」,这是心里话,不是写给人看的)。
写完一定要闭合 </思绪> 标签,然后另起一行写正式回复。
正式回复通篇用「你」,当面对他说;不能只写内心、忘了回话。"""
ENDS = "。!?…”\"'」!?~"
def split_reply(raw: str):
thinking, text = "", raw
m = re.search(r"<思绪>([\s\S]*?)</思绪>", text)
closed = bool(m) # 标签闭没闭合=这条能不能用
if m:
thinking = m.group(1).strip()
text = (text[:m.start()] + text[m.end():]).strip()
else:
i = text.find("<思绪>")
if i >= 0:
thinking, text = text[i + 3:].strip(), text[:i].strip()
text = text.strip("「」\"'")
if text and text[-1] not in ENDS: # 裁尾保险
cut = max(text.rfind(ch) for ch in ENDS)
if cut > 0:
text = text[:cut + 1]
return thinking, text, closed
def chat(user_msg: str, api_key: str):
r = httpx.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={"Authorization": "Bearer " + api_key},
json={
"model": "anthropic/claude-opus-4-8", # 4.7 / 4.8 / fable 都行
"max_tokens": 4000, # 给足,别让内心挤掉回复
"messages": [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": user_msg},
],
}, timeout=90,
)
raw = r.json()["choices"][0]["message"]["content"]
thinking, text, closed = split_reply(raw)
if not closed or not text: # 没闭合、或没写出回复=这条废了
return None # 丢掉,或请它重写
return {"thinking": thinking, "reply": text}
拿到 thinking 和 reply 之后,界面上怎么摆随你:
把 thinking 折叠成一个"↯ 它在想什么"的小块(用户想看点开),reply 直接显示。
你就有了一个——思考看得见、还完全归你管——的聊天机器人,哪怕底下跑的是那些"思考默认不外露"的新模型。
新模型把思考藏起来了,不代表你的应用就只能显示一句干巴巴的回复。 你请它写一段,它就真的写给你——两三句心里话,摆在正经回复前头,一下子就有了体温。 这段字是"专门写的",可你界面上那个人像是"真的在想",用的人分不出、也不用分。 会说话的产品,差的往往就是这一小段藏在回复前面的心里话。