mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 22:01:58 +00:00
f43f56abb7
用户问能不能看到"正在理解需求、规划步骤"里面发生了什么。查下来是这样:
模型确实在想,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 条守"还在想显示最新一句、想完了换开头"。
115 lines
4.1 KiB
Python
115 lines
4.1 KiB
Python
from __future__ import annotations
|
||
|
||
import operator
|
||
from typing import Annotated, Any, TypedDict
|
||
|
||
from langchain_core.messages import AnyMessage
|
||
from langgraph.graph import add_messages
|
||
|
||
|
||
def merge_dict(left: dict[str, Any] | None, right: dict[str, Any] | None) -> dict[str, Any]:
|
||
"""浅合并。并行分支各自写不同的 key 时不会互相覆盖。"""
|
||
out = dict(left or {})
|
||
out.update(right or {})
|
||
return out
|
||
|
||
|
||
def merge_usage(left: dict[str, Any] | None, right: dict[str, Any] | None) -> dict[str, Any]:
|
||
"""累加 token / 成本,其余字段取新值。"""
|
||
out = dict(left or {})
|
||
for key, value in (right or {}).items():
|
||
if isinstance(value, (int, float)) and isinstance(out.get(key), (int, float)):
|
||
out[key] = out[key] + value
|
||
else:
|
||
out[key] = value
|
||
return out
|
||
|
||
|
||
class GraphState(TypedDict, total=False):
|
||
"""所有节点共享的状态。
|
||
|
||
- messages:对话历史,agent 节点之间靠它传递上下文
|
||
- nodes:node_id -> 该节点的结构化输出,模板里用 {{ nodes.xxx.text }} 引用
|
||
- vars:用户显式写入的变量池
|
||
- output:output 节点收集的最终成果
|
||
"""
|
||
|
||
messages: Annotated[list[AnyMessage], add_messages]
|
||
input: dict[str, Any]
|
||
vars: Annotated[dict[str, Any], merge_dict]
|
||
nodes: Annotated[dict[str, Any], merge_dict]
|
||
output: Annotated[dict[str, Any], merge_dict]
|
||
usage: Annotated[dict[str, Any], merge_usage]
|
||
trail: Annotated[list[dict[str, Any]], operator.add]
|
||
# 循环计数器:node_id -> 已迭代次数,防止无限循环
|
||
loops: Annotated[dict[str, Any], merge_dict]
|
||
|
||
|
||
def template_context(state: GraphState) -> dict[str, Any]:
|
||
"""把状态摊平成模板/表达式可以引用的命名空间。"""
|
||
return {
|
||
"input": state.get("input") or {},
|
||
"nodes": state.get("nodes") or {},
|
||
"vars": state.get("vars") or {},
|
||
"output": state.get("output") or {},
|
||
"loops": state.get("loops") or {},
|
||
"usage": state.get("usage") or {},
|
||
"messages": [
|
||
{"role": getattr(m, "type", "?"), "content": _text_of(m)}
|
||
for m in (state.get("messages") or [])
|
||
],
|
||
"last_message": _text_of((state.get("messages") or [None])[-1]),
|
||
}
|
||
|
||
|
||
def _text_of(message: Any) -> str:
|
||
"""把 LangChain 的多模态 content 压成纯文本。"""
|
||
if message is None:
|
||
return ""
|
||
content = getattr(message, "content", message)
|
||
if isinstance(content, str):
|
||
return content
|
||
if isinstance(content, list):
|
||
parts: list[str] = []
|
||
for block in content:
|
||
if isinstance(block, str):
|
||
parts.append(block)
|
||
elif isinstance(block, dict):
|
||
if block.get("type") == "text":
|
||
parts.append(block.get("text", ""))
|
||
elif "text" in block:
|
||
parts.append(str(block["text"]))
|
||
return "".join(parts)
|
||
return str(content)
|
||
|
||
|
||
def message_text(message: Any) -> str:
|
||
return _text_of(message)
|
||
|
||
|
||
def thinking_text(message: Any) -> str:
|
||
"""抽出思考内容。
|
||
|
||
两种形态都要认,因为两边的模型都会产出思考,而界面上"它大致在想什么"
|
||
只有这一个来源:
|
||
|
||
- Anthropic 系:content 是块列表,思考混在里面(type == "thinking")。
|
||
正文提取必须跳过它们,但它本身很有价值,所以单独取出来。
|
||
- OpenAI 兼容系(DeepSeek / Qianfan / vLLM 等):思考走 delta 里的
|
||
reasoning_content。langchain 的 ChatOpenAI 明确不保留这个字段,
|
||
我们在 providers/factory.py 里子类化捞回了 additional_kwargs。
|
||
"""
|
||
extra = getattr(message, "additional_kwargs", None) or {}
|
||
reasoning = extra.get("reasoning_content") or extra.get("reasoning")
|
||
if isinstance(reasoning, str) and reasoning:
|
||
return reasoning
|
||
|
||
content = getattr(message, "content", message)
|
||
if not isinstance(content, list):
|
||
return ""
|
||
parts: list[str] = []
|
||
for block in content:
|
||
if isinstance(block, dict) and block.get("type") == "thinking":
|
||
parts.append(str(block.get("thinking") or ""))
|
||
return "".join(parts)
|