☰
DeepSeek Harness 小白入门 35:接入 LangChain 等框架时,推理字段被中间层吞掉怎么办
2026/10/3 19:27:59 网站建设 项目流程

1. 推理字段为什么会在 LangChain 里凭空消失

你直连 DeepSeek API 的时候,reasoning_content明明躺在响应里,换成 LangChain 的ChatOpenAI一包,response.content只剩正文,推理字段像被谁顺手删了。这不是模型的问题,也不是你 Key 的问题,而是中间层在把响应对象重新组装成AIMessage时,只挑了它认识的字段,剩下的全丢了。

先把概念说清楚。DeepSeek Harness 在这里扮演的是协议适配层的角色,它负责把 DeepSeek 的思考模式、工具调用、用量字段规范成一套稳定结构;LangChain 则是更上层的编排框架,它有自己的消息模型BaseMessage、AIMessage、ToolMessage。当请求从 LangChain 出发,经过ChatOpenAI或ChatDeepSeek这类封装,再打到 API,返回时又要从原始 JSON 反向构造成AIMessage。每一次「重建消息」都是一次字段过滤的机会,reasoning_content就是最容易被过滤掉的那个。

适合谁看这篇?刚上手 LangChain 接入 DeepSeek、发现推理内容拿不到、或者做 Agent 时工具调用轮次里推理字段莫名消失的开发者。你不需要先精通 LangChain 源码,只要会写 Python、能跑通一次invoke,就能跟着定位。

我先把结论摆出来:推理字段丢失几乎从不发生在模型侧,而是发生在「响应解析」和「消息序列化」这两个边界上。你要做的不是换模型,而是在这两个边界上加探针,看字段到底在哪一步没的。

具体来说,LangChain 吞字段有三个典型位置。第一是ChatOpenAI的_convert_dict_to_message,它默认只读content、tool_calls、function_call,reasoning_content不在白名单里。第二是流式模式下_convert_delta_to_message_chunk,增量 chunk 里的推理片段如果没有对应字段映射,会被直接跳过。第三是你自己写的RunnableLambda或输出解析器,把AIMessage转成 dict 时用了model_dump()的默认排除规则。

注意:不同版本的 LangChain 对additional_kwargs的处理不一样。老版本会把未知字段塞进additional_kwargs,新版本可能直接丢弃。所以「我上次还能拿到」不代表这次还能拿到,版本一定要锁。

下面这张表帮你快速判断字段丢在哪一层:

现象更可能的层先做什么
content有值,reasoning_content为 None响应解析层打印原始response.json()
流式下推理片段完全看不到delta 转换层关流式对比一次
工具轮次里推理字段消失消息序列化层检查AIMessage构造
直连有、LangChain 无中间层封装加自定义字段映射
换模型后突然没有模型/参数层确认 thinking 是否开启

理解了这个分层,你就不会一上来就怀疑 API。接下来我带你用 TaoToken 作为统一入口,把直连和经中间层的两条链路摆在一起对比,字段在哪一步掉的,一眼就能看出来。

2. TaoToken 前置准备与 LangChain 接入配置

在动手之前,先把入口统一。我这边习惯用 TaoToken 作为 API 入口,原因是它把模型对话、Coding Plan、控制台和 API Keys 都收在一个地方,切换模型和排查请求都省事。你需要先拿到一个可用的 Key,再去控制台确认要调的模型 ID。

第一步,打开模型对话页面感受一下原始返回长什么样,地址是 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=langchain_reasoning_field&utm_campaign=rewrite 。在这里发一条带思考的请求,你能直接看到reasoning_content和content是分开的两个字段,这就是后面要保住的原始形态。

第二步,去 API Keys 页面创建一个 Key,地址是 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=langchain_reasoning_field&utm_campaign=rewrite 。创建后立刻复制,页面不会二次展示。这个 Key 只放在环境变量里,别写进代码。

第三步,确认 Base URL。TaoToken 的 API 入口是 https://taotoken.net/api ,注意这个地址后面不加任何 UTM 参数,直接作为base_url使用。LangChain 的ChatOpenAI会在后面拼/chat/completions,所以你不要自己再加/v1,否则会变成双路径。

第四步,装依赖。建议单独建虚拟环境,Python 3.10 以上:

python -m venv venv source venv/bin/activate pip install langchain langchain-openai openai

版本上,langchain-openai建议 0.1.x 以上,openaiSDK 1.x。装完先pip show langchain-openai记下版本号,后面排查要用。

第五步,设置环境变量。不要用export明文写在终端历史里,用.env文件配合python-dotenv,或者直接在受控环境注入:

export TAOTOKEN_API_KEY="你的Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"

到这里前置就齐了。如果你后面要做长期编码或 Agent 编排,可以顺手看一下 Coding Plan 页面 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=langchain_reasoning_field&utm_campaign=rewrite ,它更适合多轮工具调用的场景。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=langchain_reasoning_field&utm_campaign=rewrite ,遇到参数不确定时以文档为准。

提示:TaoToken 是统一 API 入口,不是编辑器替代品,也不要把生产数据库直连进去。它解决的是调用入口和字段透传问题,业务权限仍然由你的应用自己管。

3. 可复制的 LangChain 配置与字段透传写法

这一节是核心。我给你一份可以直接跑的配置,重点在「让推理字段活下来」。先看最基础的ChatOpenAI配置,注意model_kwargs里要把思考模式打开,并且用extra_body传 DeepSeek 特有的参数。

import os from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="deepseek-v4-flash", api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], temperature=0.6, max_tokens=512, model_kwargs={ "extra_body": {"thinking": {"type": "enabled"}}, }, )

这段配置本身没问题,但invoke之后你会发现reasoning_content不在AIMessage的标准字段里。默认情况下它可能被塞进additional_kwargs,也可能被丢掉,取决于版本。所以我们要做两件事:一是显式读取additional_kwargs,二是如果它没被保留,就自己接管响应解析。

先看默认行为下怎么把字段捞出来:

resp = llm.invoke("用不超过 80 字解释推理字段透传") print("content:", resp.content) print("additional_kwargs:", resp.additional_kwargs) print("response_metadata:", resp.response_metadata)

如果additional_kwargs里有reasoning_content,说明这一层没吞,你只要在业务代码里读它就行。如果为空,说明_convert_dict_to_message把它过滤了,这时候有两个选择:升级/降级 LangChain 版本,或者用自定义子类覆盖转换逻辑。

我更推荐自定义子类,可控性最强。下面这个ReasoningChatOpenAI覆盖了_create_chat_result,把原始响应里的reasoning_content手动塞进additional_kwargs:

from typing import Any, Dict from langchain_openai import ChatOpenAI from langchain_core.messages import AIMessage from langchain_core.outputs import ChatGeneration, ChatResult class ReasoningChatOpenAI(ChatOpenAI): def _create_chat_result(self, response: Dict[str, Any]) -> ChatResult: generations = [] for choice in response.get("choices", []): message = choice.get("message", {}) reasoning = message.get("reasoning_content") ai_msg = AIMessage( content=message.get("content") or "", additional_kwargs={ "reasoning_content": reasoning, "raw_tool_calls": message.get("tool_calls"), }, response_metadata={ "finish_reason": choice.get("finish_reason"), "model": response.get("model"), }, ) generations.append(ChatGeneration(message=ai_msg)) usage = response.get("usage") or {} return ChatResult( generations=generations, llm_output={ "token_usage": usage, "model_name": response.get("model"), }, )

用的时候把ChatOpenAI换成ReasoningChatOpenAI即可,其余参数不变。这样无论 LangChain 内部怎么改,reasoning_content都会稳定落在additional_kwargs里。

如果你用的是ChatDeepSeek而不是ChatOpenAI,思路一样,但要注意它的字段名可能不同。有些版本用reasoning_content,有些用reasoning。你可以先打印一次原始响应确认:

import httpx, os, json raw = httpx.post( f"{os.environ['TAOTOKEN_BASE_URL']}/chat/completions", headers={"Authorization": f"Bearer {os.environ['TAOTOKEN_API_KEY']}"}, json={ "model": "deepseek-v4-flash", "messages": [{"role": "user", "content": "解释一下字段透传"}], "max_tokens": 256, "thinking": {"type": "enabled"}, }, timeout=60, ) print(json.dumps(raw.json(), ensure_ascii=False, indent=2))

这一步是「直连探针」,它告诉你原始字段长什么样。后面所有对比都以这个为准。

工具调用场景还要多一层。tool_calls在流式和聚合模式下结构不同,推理字段往往和工具调用绑在同一个 message 里。如果你在 Agent 循环里把AIMessage转成 dict 再传回去,务必保留additional_kwargs,否则下一轮模型看不到自己之前的推理,行为会漂移。

def to_api_message(msg: AIMessage) -> dict: return { "role": "assistant", "content": msg.content, "reasoning_content": msg.additional_kwargs.get("reasoning_content"), "tool_calls": msg.additional_kwargs.get("raw_tool_calls"), }

这份配置和透传写法,就是整篇的骨架。接下来验证它到底有没有生效。

4. 直连与经中间层的对比验证

验证的核心思路很简单:同一个请求,走两条链路,把返回的字段结构摆在一起比。一条是直连 TaoToken API,一条是经 LangChain。如果直连有reasoning_content而 LangChain 没有,问题就在中间层;如果两条都没有,那要回头查参数。

先写直连版本,用httpx直接打:

import httpx, os, json payload = { "model": "deepseek-v4-flash", "messages": [{"role": "user", "content": "用三步解释字段透传"}], "max_tokens": 300, "thinking": {"type": "enabled"}, } direct = httpx.post( f"{os.environ['TAOTOKEN_BASE_URL']}/chat/completions", headers={"Authorization": f"Bearer {os.environ['TAOTOKEN_API_KEY']}"}, json=payload, timeout=60, ).json() msg = direct["choices"][0]["message"] print("直连 reasoning_content:", bool(msg.get("reasoning_content"))) print("直连 content 长度:", len(msg.get("content") or "")) print("直连 finish_reason:", direct["choices"][0].get("finish_reason"))

再写 LangChain 版本,用上一节的自定义类:

llm = ReasoningChatOpenAI( model="deepseek-v4-flash", api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], max_tokens=300, model_kwargs={"extra_body": {"thinking": {"type": "enabled"}}}, ) resp = llm.invoke("用三步解释字段透传") print("LangChain reasoning_content:", bool(resp.additional_kwargs.get("reasoning_content"))) print("LangChain content 长度:", len(resp.content)) print("LangChain finish_reason:", resp.response_metadata.get("finish_reason"))

跑完把两组输出并排看。正常情况下,两边reasoning_content都应该为 True,finish_reason都是stop。如果 LangChain 那边是 False,说明你的自定义类没生效,或者model_kwargs里的extra_body没传进去。

流式模式要单独验一次,因为 delta 聚合是另一个吞字段的重灾区:

for chunk in llm.stream("用两句话解释流式推理字段"): reasoning = chunk.additional_kwargs.get("reasoning_content") if reasoning: print("[reasoning]", reasoning[:40]) if chunk.content: print("[content]", chunk.content[:40])

流式下如果只看到 content 没有 reasoning,说明_convert_delta_to_message_chunk没映射推理字段。这时候要么改用非流式做推理展示,要么在自定义类里覆盖 delta 转换。我实测下来,非流式做推理字段验证更稳,流式更适合最终正文输出。

一份合格的验证日志应该长这样:

[日期] 2026-08-14 [入口] TaoToken https://taotoken.net/api [模型] deepseek-v4-flash [直连] reasoning_content=True, finish_reason=stop [LangChain] reasoning_content=True, finish_reason=stop [版本] langchain-openai 0.1.x [结论] 字段透传成功,中间层未吞字段

如果两边不一致,日志里要写清楚差在哪一步,是解析层还是序列化层。这样你下次换版本时,直接对比日志就能发现回归。

5. 常见报错与吞字段排查

这一节按真实报错来。你大概率会遇到下面几种,我逐个给排查路径。

401 Unauthorized。先查 Key 有没有带Bearer前缀,再查环境变量名有没有拼错。LangChain 里api_key传的是纯 Key,不要自己加前缀。如果直连能通、LangChain 报 401,多半是base_url拼错了,比如多加了/v1变成https://taotoken.net/api/v1/chat/completions,而正确路径是https://taotoken.net/api/chat/completions。

local proxy failed / connection error。这类报错通常是本地网络环境或代理配置干扰。检查你的HTTP_PROXY、HTTPS_PROXY环境变量是否指向了不可用的地址,LangChain 底层用的 httpx 会读这些变量。清掉再试:

unset HTTP_PROXY HTTPS_PROXY ALL_PROXY

reading choices 报错。这通常意味着响应结构和你预期的不一样,比如返回的是错误对象而不是正常 completion。先打印原始response.json(),看有没有error字段。常见原因是模型 ID 写错,或者thinking参数格式不对。DeepSeek 的思考参数在不同版本里可能是thinking也可能是extra_body.thinking,以接入文档为准。

OAuth / 认证相关报错。如果你用的是某些需要 OAuth 的封装,注意 TaoToken 走的是标准 API Key 认证,不需要 OAuth 流程。看到 OAuth 字样,先确认你用的 SDK 是不是被配置成了别的认证模式。

推理字段为 None 但不报错。这是最隐蔽的。排查顺序:先直连确认原始响应有字段,再打印resp.additional_kwargs看有没有被保留,最后检查你的输出解析器有没有把它过滤掉。如果你用了JsonOutputParser或自定义RunnableLambda,很可能在model_dump()时被排除。

对照表再放一次,方便你快速定位:

报错/现象可能原因处理
401Key 或 base_url 错检查前缀与路径
local proxy failed代理环境变量unset 代理变量
reading choices响应非预期结构打印原始 JSON
OAuth 相关认证模式错改回 API Key
reasoning 为 None解析层过滤自定义_create_chat_result
流式无推理delta 未映射改非流式或覆盖 delta

如果你用的是 CC Switch、Cline MCP 或 Codex 这类工具,配置里必须写全三件套:Base URL 填https://taotoken.net/api,Key 填你的 API Key,Model ID 填实际模型名。少任何一个都会导致请求失败或字段异常。Cline MCP 场景下还要注意工具调用的消息序列化,推理字段要在additional_kwargs里跟着走。

注意:任何中间层只要重建消息,就必须验证字段没有丢失。不要因为一次成功就跳过验证,版本升级后要重跑对比。

6. 把字段透传固化进你的接入流程

排查完不是终点,把验证固化成流程才是。我的做法是在项目里放一个tests/test_reasoning_passthrough.py,每次升级 LangChain 或换模型就跑一次。测试内容就是上一节的对比逻辑,断言两边reasoning_content都非空。

def test_reasoning_not_dropped(): direct = call_direct() via_lc = call_langchain() assert direct["choices"][0]["message"].get("reasoning_content") assert via_lc.additional_kwargs.get("reasoning_content")

这样字段一旦被吞,CI 会直接报红,不用等到线上才发现。

另外,日志里永远记三样东西:模型 ID、finish_reason、usage。推理字段有没有丢,配合finish_reason一起看最准。如果finish_reason=length,推理可能被截断,这时候字段为空不代表中间层吞了,而是输出预算不够。

如果你要做长期编码或 Agent 编排,建议把入口统一到 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=langchain_reasoning_field&utm_campaign=rewrite ,它更适合多轮工具调用和推理字段的持续透传。需要再确认模型行为时,回到模型对话页面 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=langchain_reasoning_field&utm_campaign=rewrite 直连看一眼原始返回。Key 管理在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=langchain_reasoning_field&utm_campaign=rewrite ,接入细节以 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=langchain_reasoning_field&utm_campaign=rewrite 为准。

最后留一个实用技巧:把reasoning_content和content分开存,推理字段只用于调试和可观测,不要直接展示给终端用户,也不要让它参与业务判断。字段透传的目的是让你看得见模型在想什么,而不是让推理内容变成新的依赖。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询