跳到主要内容

GLM-4.6支持思考及思维链回传

· 阅读需 5 分钟

GLM-4.6在cluade code中启用思考

GLM从4.5开始就对claude code进行了支持,我之前也一直在关注,很多用户反映在claude code中无法启用思考,刚好最近收到了来自智谱的赞助,就着手进行研究。

首先根据官方文档,我们发现/chat/completions端点是默认启用思考的,但是是由模型判断是否需要进行思考

thinking object
仅 GLM-4.5 及以上模型支持此参数配置. 控制大模型是否开启思维链。

thinking.type enum<string> default:enabled
是否开启思维链(当开启后 GLM-4.6 GLM-4.5 为模型自动判断是否思考,GLM-4.5V 为强制思考), 默认: enabled.

Available options: enabled, disabled

在claude code本身大量的提示词干扰下,会严重阻碍GLM模型本身的判断机制,导致模型很少进行思考。所以我们需要对模型进行引导,让模型认为需要进行思考。但是claude-code-router作为proxy,能做的只能是修改提示词/参数。

在最开始,我尝试直接删除claude code的系统提示词,模型确实进行了思考,但是这样就无法驱动claude code。所以我们需要进行提示词注入,明确告知模型需要进行思考。

// transformer.ts
import { UnifiedChatRequest } from "../types/llm";
import { Transformer } from "../types/transformer";

export class ForceReasoningTransformer implements Transformer {
name = "forcereasoning";

async transformRequestIn(
request: UnifiedChatRequest
): Promise<UnifiedChatRequest> {
const systemMessage = request.messages.find(
(item) => item.role === "system"
);
if (Array.isArray(systemMessage?.content)) {
systemMessage.content.push({
type: "text",
text: "You are an expert reasoning model. \nAlways think step by step before answering. Even if the problem seems simple, always write down your reasoning process explicitly. \nNever skip your chain of thought. \nUse the following output format:\n<reasoning_content>(Write your full detailed thinking here.)</reasoning_content>\n\nWrite your final conclusion here.",
});
}
const lastMessage = request.messages[request.messages.length - 1];
if (lastMessage.role === "user" && Array.isArray(lastMessage.content)) {
lastMessage.content.push({
type: "text",
text: "You are an expert reasoning model. \nAlways think step by step before answering. Even if the problem seems simple, always write down your reasoning process explicitly. \nNever skip your chain of thought. \nUse the following output format:\n<reasoning_content>(Write your full detailed thinking here.)</reasoning_content>\n\nWrite your final conclusion here.",
});
}
if (lastMessage.role === "tool") {
request.messages.push({
role: "user",
content: [
{
type: "text",
text: "You are an expert reasoning model. \nAlways think step by step before answering. Even if the problem seems simple, always write down your reasoning process explicitly. \nNever skip your chain of thought. \nUse the following output format:\n<reasoning_content>(Write your full detailed thinking here.)</reasoning_content>\n\nWrite your final conclusion here.",
},
],
});
}
return request;
}
}

至于为什么让模型将思考内容放入reasoning_content标签而不是think标签有两个原因:

  1. 直接使用think标签不能很好的激活思考,猜测是训练模型时以think标签作为数据集进行训练。
  2. 如果使用think标签,模型的推理内容会被拆分到单独的字段,这就涉及到我们接下来要说的思维链回传问题。

思维链回传

近期Minimax发布了Minimax-m2,与此同时,他们还发布了一篇文章介绍思维链回传。但是太阳底下无新鲜事,刚好借此来剖析一下。

  1. 我们首先来看一下为什么需要回传思维链? Minimax在文章中说的是Chat Completion API不支持在后续请求中传递推理内容。我们知道ChatGPT是最先支持推理的,但是OpenAI最初没有开放思维链给用户,所以对于Chat Completion API来讲并不需要支持思维链相关的东西。就连CoT的字段也是DeepSeek率先在Chat Completion API中加入的。

  2. 我们真的需要这些字段吗? 如果没有这些字段会怎么样?会影响到模型的思考吗?可以查看一下sglang的源码发现思维链的信息原本就会在消息中按照特定的标记进行输出,假如我们不对其进行拆分,正常情况下在下轮对话中会自然包含这些信息。所以需要思维链回传的原因就是我们对模型的思维链内容进行拆分。

我用上面不到40行的代码完成了对GLM-4.5/6支持思考以及思维链回传的简单探索(单纯是因为没时间做拆分,完全可以在transformer中响应时先做拆分,请求时再进行合并,这样对cc前端的展示适配会更好),如果你有什么更好的想法也欢迎与我联系。

或许我们能在 Router 中做更多事情

· 阅读需 5 分钟

自从claude-code-router发布以来,我收到了很多用户的反馈,至今还有不少的 issues 未处理。其中大多都是关于不同的供应商的支持和deepseek模型调用工具不积极的问题。 之前开发这个项目主要是为了我自己能以较低成本使用上claude code,所以一开始的设计并没有考虑到多供应商的情况。在实际的排查问题中,我发现尽管市面上所有的供应商几乎都宣称兼容OpenAI格式调用,即通过/chat/compeletions接口调用,但是其中的细节差异非常多。例如:

  1. Gemini 的工具参数类型是 string 时,format参数只支持datedate-time,并且没有工具调用 ID。

  2. OpenRouter 需要使用cache_control进行缓存。

  3. DeepSeek 官方 API 的 max_output 为 8192,而火山引擎的会更大。

除了这些问题之外,还有一些其他的小的供应商,他们或多或少参数都有点问题。于是,我打算开发一个新的项目musistudio/llms来处理这种不同服务商的兼容问题。该项目使用 OpenAI 格式为基础的通用格式,提供了一个Transformer接口,该接口用于处理转换请求和响应。当我们给不同的服务商都实现了Transformer后,我们可以实现不同服务商的混合调用。比如我在AnthropicTransformer中实现了Anthropic <-> OpenAI格式的互相转换,并监听了/v1/messages端点,在GeminiTransformer中实现了Gemini <-> OpenAI格式的互相转换,并监听了/v1beta/models/:modelAndAction端点,当他们的请求和响应都被转换成一个通用格式的时候,就可以实现他们的互相调用。

AnthropicRequest -> AnthropicTransformer -> OpenAIRequest -> GeminiTransformer -> GeminiRequest -> GeminiServer
GeminiReseponse -> GeminiTransformer -> OpenAIResponse -> AnthropicTransformer -> AnthropicResponse

虽然使用中间层抹平差异可能会带来一些性能问题,但是该项目最初的目的是为了让claude-code-router支持不同的供应商。

至于deepseek模型调用工具不积极的问题,我发现这是由于deepseek在长上下文中的指令遵循不佳导致的。现象就是刚开始模型会主动调用工具,但是在经过几轮对话后模型只会返回文本。一开始的解决方案是通过注入一个系统提示词告知模型需要积极去使用工具以解决用户的问题,但是后面测试发现在长上下文中模型会遗忘该指令。 查看deepseek文档后发现模型支持tool_choice参数,可以强制让模型最少调用 1 个工具,我尝试将该值设置为required,发现模型调用工具的积极性大大增加,现在我们只需要在合适的时候取消这个参数即可。借助musistudio/llmsTransformer可以让我们在发送请求前和收到响应后做点什么,所以我参考claude codePlan Mode,实现了一个使用与deepseekTool Mode

export class TooluseTransformer implements Transformer {
name = "tooluse";

transformRequestIn(request: UnifiedChatRequest): UnifiedChatRequest {
if (request.tools?.length) {
request.messages.push({
role: "system",
content: `<system-reminder>Tool mode is active. The user expects you to proactively execute the most suitable tool to help complete the task.
Before invoking a tool, you must carefully evaluate whether it matches the current task. If no available tool is appropriate for the task, you MUST call the \`ExitTool\` to exit tool mode — this is the only valid way to terminate tool mode.
Always prioritize completing the user's task effectively and efficiently by using tools whenever appropriate.</system-reminder>`,
});
request.tool_choice = "required";
request.tools.unshift({
type: "function",
function: {
name: "ExitTool",
description: `Use this tool when you are in tool mode and have completed the task. This is the only valid way to exit tool mode.
IMPORTANT: Before using this tool, ensure that none of the available tools are applicable to the current task. You must evaluate all available options — only if no suitable tool can help you complete the task should you use ExitTool to terminate tool mode.
Examples:
1. Task: "Use a tool to summarize this document" — Do not use ExitTool if a summarization tool is available.
2. Task: "What's the weather today?" — If no tool is available to answer, use ExitTool after reasoning that none can fulfill the task.`,
parameters: {
type: "object",
properties: {
response: {
type: "string",
description:
"Your response will be forwarded to the user exactly as returned — the tool will not modify or post-process it in any way.",
},
},
required: ["response"],
},
},
});
}
return request;
}

async transformResponseOut(response: Response): Promise<Response> {
if (response.headers.get("Content-Type")?.includes("application/json")) {
const jsonResponse = await response.json();
if (
jsonResponse?.choices[0]?.message.tool_calls?.length &&
jsonResponse?.choices[0]?.message.tool_calls[0]?.function?.name ===
"ExitTool"
) {
const toolArguments = JSON.parse(toolCall.function.arguments || "{}");
jsonResponse.choices[0].message.content = toolArguments.response || "";
delete jsonResponse.choices[0].message.tool_calls;
}

// Handle non-streaming response if needed
return new Response(JSON.stringify(jsonResponse), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} else if (response.headers.get("Content-Type")?.includes("stream")) {
// ...
}
return response;
}
}

该工具将始终让模型至少调用一个工具,如果没有合适的工具或者任务已完成可以调用ExitTool来退出工具模式,因为是依靠tool_choice参数实现的,所以仅适用于支持该参数的模型。经过测试,该工具能显著增加deepseek的工具调用次数,弊端是可能会有跟任务无关或者没有必要的工具调用导致增加任务执行事件和消耗的 token 数。

这次更新仅仅是在 Router 中实现一个agent的一次小探索,或许还能做更多其他有趣的事也说不定...

项目初衷及原理

· 阅读需 6 分钟

早在 Claude Code 发布的第二天(2025-02-25),我就尝试并完成了对该项目的逆向。当时要使用 Claude Code 你需要注册一个 Anthropic 账号,然后申请 waitlist,等待通过后才能使用。但是因为众所周知的原因,Anthropic 屏蔽了中国区的用户,所以通过正常手段我无法使用,通过已知的信息,我发现:

  1. Claude Code 使用 npm 进行安装,所以很大可能其使用 Node.js 进行开发。
  2. Node.js 调试手段众多,可以简单使用console.log获取想要的信息,也可以使用--inspect将其接入Chrome Devtools,甚至你可以使用d8去调试某些加密混淆的代码。

由于我的目标是让我在没有 Anthropic 账号的情况下使用Claude Code,我并不需要获得完整的源代码,只需要将Claude Code请求 Anthropic 模型时将其转发到我自定义的接口即可。接下来我就开启了我的逆向过程:

  1. 首先安装Claude Code
npm install -g @anthropic-ai/claude-code
  1. 安装后该项目被放在了~/.nvm/versions/node/v20.10.0/lib/node_modules/@anthropic-ai/claude-code中,因为我使用了nvm作为我的 node 版本控制器,当前使用node-v20.10.0,所以该路径会因人而异。
  2. 找到项目路径之后可通过 package.json 分析包入口,内容如下:
{
"name": "@anthropic-ai/claude-code",
"version": "1.0.24",
"main": "sdk.mjs",
"types": "sdk.d.ts",
"bin": {
"claude": "cli.js"
},
"engines": {
"node": ">=18.0.0"
},
"type": "module",
"author": "Boris Cherny <boris@anthropic.com>",
"license": "SEE LICENSE IN README.md",
"description": "Use Claude, Anthropic's AI assistant, right from your terminal. Claude can understand your codebase, edit files, run terminal commands, and handle entire workflows for you.",
"homepage": "https://github.com/anthropics/claude-code",
"bugs": {
"url": "https://github.com/anthropics/claude-code/issues"
},
"scripts": {
"prepare": "node -e \"if (!process.env.AUTHORIZED) { console.error('ERROR: Direct publishing is not allowed.\\nPlease use the publish-external.sh script to publish this package.'); process.exit(1); }\"",
"preinstall": "node scripts/preinstall.js"
},
"dependencies": {},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "^0.33.5",
"@img/sharp-darwin-x64": "^0.33.5",
"@img/sharp-linux-arm": "^0.33.5",
"@img/sharp-linux-arm64": "^0.33.5",
"@img/sharp-linux-x64": "^0.33.5",
"@img/sharp-win32-x64": "^0.33.5"
}
}

其中"claude": "cli.js"就是我们要找的入口,打开 cli.js,发现代码被压缩混淆过了。没关系,借助webstormFormate File功能可以重新格式化,让代码变得稍微好看一点。就像这样: webstorm-formate-file

现在,你可以通过阅读部分代码来了解Claude Code的内容工具原理与提示词。你也可以在关键地方使用console.log来获得更多信息,当然,也可以使用Chrome Devtools来进行断点调试,使用以下命令启动Claude Code:

NODE_OPTIONS="--inspect-brk=9229" claude

该命令会以调试模式启动Claude Code,并将调试的端口设置为9229。这时候通过 Chrome 访问chrome://inspect/即可看到当前的Claude Code进程,点击inspect即可进行调试。 chrome-devtools chrome-devtools

通过搜索关键字符api.anthropic.com很容易能找到Claude Code用来发请求的地方,根据上下文的查看,很容易发现这里的baseURL可以通过环境变量ANTHROPIC_BASE_URL进行覆盖,apiKeyauthToken也同理。 search

到目前为止,我们获得关键信息:

  1. 可以使用环境变量覆盖Claude CodeBaseURLapiKey的配置

  2. Claude Code使用Anthropic API的规范

所以我们需要:

  1. 实现一个服务用来将OpenAI API的规范转换成Anthropic API格式。

  2. 启动Claude Code之前写入环境变量将baseURL指向到该服务。

于是,claude-code-router就诞生了,该项目使用Express.js作为 HTTP 服务,实现/v1/messages端点,使用middlewares处理请求/响应的格式转换以及请求重写功能(可以用来重写 Claude Code 的提示词以针对单个模型进行调优)。 在 2 月份由于DeepSeek全系列模型对Function Call的支持不佳导致无法直接使用DeepSeek模型,所以在当时我选择了qwen-max模型,一切表现的都很好,但是qwen-max不支持KV Cache,意味着我要消耗大量的 token,但是却无法获取Claude Code原生的体验。 所以我又尝试了Router模式,即使用一个小模型对任务进行分发,一共分为四个模型:routertoolthinkcoder,所有的请求先经过一个免费的小模型,由小模型去判断应该是进行思考还是编码还是调用工具,再进行任务的分发,如果是思考和编码任务将会进行循环调用,直到最终使用工具写入或修改文件。但是实践下来发现免费的小模型不足以很好的完成任务的分发,再加上整个 Agnet 的设计存在缺陷,导致并不能很好的驱动Claude Code。 直到 5 月底,Claude Code被正式推出,这时DeepSeek全系列模型(R1 于 05-28)均支持Function Call,我开始重新设计该项目。在与 AI 的结对编程中我修复了之前的请求和响应转换问题,在某些场景下模型输出 JSON 响应而不是Function Call。这次直接使用DeepSeek-v3模型,它工作的比我想象中要好:能完成绝大多数工具调用,还支持用步骤规划解决任务,最关键的是DeepSeek的价格不到claude Sonnet 3.5的十分之一。正式发布的Claude Code对 Agent 的组织也不同于测试版,于是在分析了Claude Code的请求调用之后,我重新组织了Router模式:现在它还是四个模型:默认模型、backgroundthinklongContext

  • 默认模型作为最终的兜底和日常处理

  • background是用来处理一些后台任务,据 Anthropic 官方说主要用Claude Haiku 3.5模型去处理一些小任务,如俳句生成和对话摘要,于是我将其路由到了本地的ollama服务。

  • think模型用于让Claude Code进行思考或者在Plan Mode下使用,这里我使用的是DeepSeek-R1,由于其不支持推理成本控制,所以ThinkUltraThink是一样的逻辑。

  • longContext是用于处理长下上文的场景,该项目会对每次请求使用tiktoken实时计算上下文长度,如果上下文大于32K则使用该模型,旨在弥补DeepSeek在长上下文处理不佳的情况。

以上就是该项目的发展历程以及我的一些思考,通过巧妙的使用环境变量覆盖的手段在不修改Claude Code源码的情况下完成请求的转发和修改,这就使得在可以得到 Anthropic 更新的同时使用自己的模型,自定义自己的提示词。该项目只是在 Anthropic 封禁中国区用户的情况下使用Claude Code并且达到成本和性能平衡的一种手段。如果可以的话,还是官方的Max Plan体验最好。