Files
AgentLab/backend/tests/test_thinking.py
T
ThearchyHelios f43f56abb7 把 Copilot 的思考显示出来:模型一直在想,只是被丢掉了
用户问能不能看到"正在理解需求、规划步骤"里面发生了什么。查下来是这样:
模型确实在想,reasoning 花了实打实的 token,但一路上被丢了三次。

1. langchain 的 ChatOpenAI **故意**不保留第三方的 reasoning_content——它的
   文档原话是 "Non-standard response fields added by third-party providers
   are not extracted or preserved",建议换 ChatDeepSeek 之类的专用包。可
   "用哪个供应商"在这个产品里是用户在设置页配的(DeepSeek / Qianfan /
   vLLM / OpenRouter 都可能),为每种装一个专用包不现实。在 factory 里
   子类化捞回 additional_kwargs,只加字段,不动 langchain 对正文的处理。

2. thinking_text 只认 Anthropic 的 thinking 块。补上兼容端点那一种。

3. 前端把一段思考的标题固定成开头 60 个字。思考是流式的,开头往往是
   "用户想要一个……"这类复述,之后几十秒里它一直在推进——固定显示开头
   等于把一段活的叙述冻在起点上,看着就像卡住了。改成:还在想的时候显示
   最新一句,想完了换回开头那句(那时它是一条历史记录)。

实测一次真实生成,60 秒里侧栏逐句变化:

    3s  可用数据源 bi
    9s  人工节点 approve 模式,审核
   18s  我们可以用 db_query__sales 执行 SELECT COUNT(*) FROM orders 等
   36s  更合理:rejected -> query_agent(重新
   45s  对于 bi 有 53 个对象,靠
   57s  (plan 到达,思考收尾,换回开头那句)

顺带运行期的 LLM 节点也跟着有了思考——走的是同一个 thinking_text,之前
同样只有 Anthropic 系才有。已验证流式合并后思考连成完整一段,不是只剩
最后一片。

tests/test_thinking.py 守两种形态、合并、以及"不产思考的模型不硬凑"。
check-decode 补 5 条守"还在想显示最新一句、想完了换开头"。
2026-09-18 09:55:42 +08:00

55 lines
2.4 KiB
Python

"""思考内容的提取。
界面上"它大致在想什么"只有这一个来源。两家的形态完全不同,漏掉任何一种,
那一侧的模型在编排期就只剩一行不动的"正在理解需求"——而它其实想了 50 秒。
"""
from __future__ import annotations
from langchain_core.messages import AIMessage, AIMessageChunk
from app.engine.state import message_text, thinking_text
def test_anthropic_thinking_blocks() -> None:
msg = AIMessage(content=[
{"type": "thinking", "thinking": "先看看有哪些表。"},
{"type": "text", "text": "查到 3 张表。"},
{"type": "thinking", "thinking": "再按月份聚合。"},
])
assert thinking_text(msg) == "先看看有哪些表。再按月份聚合。"
# 正文不能把思考混进来
assert thinking_text(msg) not in message_text(msg)
assert "查到 3 张表。" in message_text(msg)
def test_openai_compatible_reasoning_content() -> None:
"""DeepSeek / Qianfan / vLLM 这类兼容端点把思考放在 reasoning_content。
langchain 的 ChatOpenAI 明确不保留这个字段("Non-standard response fields
added by third-party providers are not extracted or preserved"),
providers/factory.py 子类化捞了回来。
"""
chunk = AIMessageChunk(content="", additional_kwargs={"reasoning_content": "先看看现有的节点。"})
assert thinking_text(chunk) == "先看看现有的节点。"
def test_merged_chunks_keep_whole_reasoning() -> None:
"""流式合并后思考要连成一整段,不能只剩最后一片。"""
parts = ["用户想要", "一个能查销量的流程。", "先看看现有的节点。"]
merged = AIMessageChunk(content="", additional_kwargs={"reasoning_content": parts[0]})
for p in parts[1:]:
merged = merged + AIMessageChunk(content="", additional_kwargs={"reasoning_content": p})
assert thinking_text(merged) == "".join(parts)
def test_plain_message_has_no_thinking() -> None:
"""不产思考的模型不该被硬凑出一段来。"""
assert thinking_text(AIMessage(content="就是 2")) == ""
assert thinking_text(AIMessageChunk(content="x", additional_kwargs={})) == ""
def test_empty_reasoning_is_not_reported() -> None:
"""空串不算思考,否则界面上会出现一行什么都没有的"思考"。"""
assert thinking_text(AIMessageChunk(content="", additional_kwargs={"reasoning_content": ""})) == ""