使用 vLLM 启动时 ```bash screen -U -L -Logfile /root/vllm_server.log -dmS vllm_server bash -c 'python -m vllm.entrypoints.openai.api_server \ --model /root/autodl-tmp/DeepSeek-R1-Distill-Qwen-7B \ --served-model-name deepseek-r1 \ --tensor-parallel-size 1 \ --max-model-len 131072 \ --gpu-memory-utilization 0.95 \ --port 6006' ``` 此参数可以决定对话返回时在 reasoning 中。 ``` --reasoning-parser deepseek_r1 ``` ```json { "id": "chatcmpl-a76d0e60ddd6e6cc", "object": "chat.completion", "created": 1774488794, "model": "deepseek-r1", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "\n\n以下是使用Python实现的快速排序算法的完整代码:\n\n```python\ndef quicksort(arr):\n if len(arr) <= 1:\n return arr\n pivot = arr[0]\n left = []\n right = []\n for num in arr[1:]:\n if num <= pivot:\n ", "refusal": null, "annotations": null, "audio": null, "function_call": null, "tool_calls": [], "reasoning": "嗯,用户让我用Python写一个快速排序算法。首先,我得理解用户的需求。他们可能是在学习编程,或者想快速实现一个排序函数,可能是做数据处理或者算法竞赛的练习。他们可能需要一个简洁的代码,但同时也要考虑效率的问题,因为快速排序的时间复杂度是O(n log n),对于大数组来说可能会有点慢。\n\n那我应该怎么做呢?首先,我得写出一个基本的快速排序函数。基本的步骤是选择一个 pivot,然后将数组分成两部分,大于和小于 pivot的元素,然后递归排序这两部分。\n\n选择 pivot 的方法有很多种,比如选第一个元素,最后一个元素,中间的,或者随机选。选第一个或者最后一个可能比较简单,但可能在某些情况下不高效。比如,如果数组已经是有序的,选第一个作为 pivot 可能会导致每次递归都需要处理整个数组,反而效率低下。所以,可能更好的选择是随机选 pivot 或者中间的元素。\n\n接下来,我得考虑数组的分割过程。对于分割,我可以遍历数组,将元素与 pivot 比较,把小于的放在左边,大于的放在右边。注意,这里可能会有多个 pivot,比如相等的元素,所以需要特别处理。\n\n然后,递归排序。递归的条件是子数组的长度大于等于2,否则返回。递归的base case是当子数组长度小于2时,直接返回。\n\n另外,我得考虑数组的大小。如果数组很小,比如长度为0、1或2,直接返回即可,不需要递归。这能节省一些计算时间,尤其是在处理小规模数据时。\n\n那我得注意数组的可变性。因为快速排序是基于递归的,所以需要将原数组修改,或者返回一个新的排序后的数组。如果直接修改原数组,可能会导致问题,因为分割后的子数组可能被重新排序,从而改变原数组的顺序。所以,通常情况下,快速排序应该返回一个新的列表,而不是修改原数组。或者,如果用户希望保留原数组,可以使用in-place排序,比如使用sort()方法,但这种方法在Python中对于列表来说是O(n^2)的,所以对于大数组来说效率不高。不过,快速排序在很多情况下还是比这个方法快,尤其是当数组中有大量相似元素的时候。\n\n那我得决定是否要修改原数组。如果用户希望保留原数组,那么可能需要写一个不改变原数组的版本。或者,用户可能只是需要排序,所以返回一个新的排序后的列表即可。这可能更方便,特别是当数组很大时,因为快速排序的时间复杂度对于大数组来说还是可以接受的。\n\n所以,我得考虑两种情况:一种是修改原数组,返回排序后的列表;另一种是不修改原数组,返回一个新列表。这可能取决于用户的具体需求。如果用户希望原数组不变,那么分割后的子数组可能需要重新排列,这会导致数据混乱。所以,通常情况下,快速排序应该返回一个新列表,而不是修改原数组。\n\n那我得写出代码。首先,定义一个函数quicksort,接受一个数组。然后,选择pivot,比如选第一个元素。然后,创建两个子数组,左边和右边,分别包含小于和大于 pivot 的元素。然后,递归地排序左边和右边,然后将它们拼接起来返回。\n\n那我得处理一些特殊情况,比如数组为空或者只有一个元素。或者,如果数组长度为0,返回空列表;长度为1,直接返回。这会减少递归的次数,提高效率。\n\n那我得测试一下这个算法。比如,测试一个有序数组,比如[1,2,3,4,5],快速排序应该能快速排序。或者,测试一个有大量相同元素的数组,比如[5,5,5,5],快速排序应该很快,而不需要太多递归深度。\n\n另外,我还得考虑时间复杂度。因为快速排序是平均O(n log n),但在最坏情况下,比如当数组是完全逆序的时候,时间复杂度会是O(n²),这在Python中对于大数组来说可能会比较慢。所以,用户可能需要了解这点,或者根据自己的情况选择算法。\n\n那我得写一个完整的代码,包括选择pivot、分割、递归排序,以及处理特殊情况。然后,测试一下这个代码是否正确,是否高效。\n\n综上,我应该写出一个基本的快速排序函数,可以选择不同的pivot方法,处理特殊情况,返回排序后的列表。这样用户就能轻松地使用这个函数来排序数组。\n" }, "logprobs": null, "finish_reason": "length", "stop_reason": null, "token_ids": null } ], "service_tier": null, "system_fingerprint": null, "usage": { "prompt_tokens": 20, "total_tokens": 1044, "completion_tokens": 1024, "prompt_tokens_details": null }, "prompt_logprobs": null, "prompt_token_ids": null, "kv_transfer_params": null } ``` 如果不指定则在内容中,但目前有个问题返回的标签的不完整``: ```json { "id": "chatcmpl-bf2d384d0a1e156f", "object": "chat.completion", "created": 1774489483, "model": "deepseek-r1", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "好的,我现在要帮助用户用Python编写一个快速排序算法。快速排序是一种经典的排序算法,基于分治法。我需要先回忆一下快速排序的基本步骤,然后思考如何将其转化为Python代码。\n\n首先,快速排序的基本步骤是选择一个基准元素,然后将数组分成两部分:一部分包含小于等于基准元素的元素,另一部分包含大于基准元素的元素。然后,分别对这两部分进行排序,最后合并得到排序后的数组。\n\n接下来,我需要考虑如何选择基准元素。通常,选择数组的第一个元素作为基准,或者中间元素,或者最后的元素。这里,我可能会选择第一个元素作为基准,因为这在很多情况下都比较高效。\n\n然后,我需要处理数组中的元素。对于每个元素,如果它小于基准元素,就把它放到基准元素的左边;如果它大于,放到右边。这个过程需要递归地处理子数组。\n\n在Python中,我可以定义一个函数,比如quicksort,接受一个数组作为参数。这个函数首先检查数组的长度,如果为空,返回。然后选择基准元素,比如arr[0]。接下来,遍历数组中的每个元素,将它们分成两部分:left和right。left包含所有小于基准元素的元素,right包含所有大于或等于基准元素的元素。然后,递归调用quicksort处理left和right,最后将sorted_left和sorted_right合并成最终的结果。\n\n不过,合并两个排序数组可能会浪费一些时间,因为它们都是递归生成的。为了优化,可能需要使用更高效的方法,比如双指针法,或者利用Python的列表拼接。但在这个简单的版本中,可能直接使用sorted(left) + sorted(right)会更直接,虽然时间复杂度会增加,但可能足够快。\n\n另外,我需要考虑数组的大小。对于小规模的数组,递归调用可能会导致栈溢出,所以可能需要添加一个默认排序函数,或者在递归深度足够的情况下才使用快速排序。\n\n在编写代码时,我会先写一个大致的框架:\n\ndef quicksort(arr):\n if len(arr) <= 1:\n return arr\n pivot = arr[0]\n left = [x for x in arr[1:] if x <= pivot]\n right = [x for x in arr[1:] if x > pivot]\n return quicksort(left) + quicksort(right)\n\n这样,代码看起来很简洁,但可能在某些情况下效率较低,因为每次分割都会生成两个子数组,然后递归处理。此外,使用列表推导式生成left和right,可能会带来一些性能上的优化,因为它们直接创建新的列表而不需要生成新对象。\n\n不过,我发现这可能忽略了元素的顺序问题。比如,如果基准元素是中间值,那么分割后的left和right可能仍然需要重新排序。这可能需要更复杂的处理,比如交换位置,或者在分割后再次调用快速排序。\n\n为了更高效,可能需要交换基准元素的位置。比如,当遇到元素大于基准元素时,交换它们的位置,然后继续分割。这可能有助于减少递归深度,并提高效率。\n\n不过,这可能让代码变得复杂,因为需要引入交换操作。例如:\n\ndef quicksort(arr):\n if len(arr) <= 1:\n return arr\n pivot = arr[0]\n left = []\n right = []\n for x in arr[1:]:\n if x <= pivot:\n left.append(x)\n else:\n right.append(x)\n # 交换基准元素和当前元素的位置\n arr[0], x = x, arr[0]\n return quicksort(left) + [pivot] + quicksort(right)\n\n这样,交换操作会确保基准元素被移动到正确的位置,从而可能减少分割次数。这可能提高算法的效率。\n\n此外,我需要考虑基准元素的选择。比如,选择中间元素作为基准,或者更复杂的策略,比如随机选择基准元素。这可能对某些情况来说更好,但可能增加代码的复杂度。\n\n综上所述,我需要编写一个快速排序的Python代码,尽可能地高效和简洁。可能需要考虑基准元素的选择、分割方式、合并方式,以及优化方法。\n\n最后,我可能会测试这个代码是否正确,是否能够处理不同的输入情况,比如已经排序的数组,已经逆序的数组,以及空数组等。\n\n\n以下是使用Python编写一个快速排序算法的示例代码:\n\n```python\ndef quicksort(arr):\n if len(arr) <= 1:\n return arr\n pivot = arr[0]\n left = [x for x in arr[1:] if x <= pivot]\n right = [x for x in arr[1:] if x > pivot]\n return quicksort(left) + quicksort(right)\n\n# 示例使用\narr = [3, 6, 8, 10, 1, 2", "refusal": null, "annotations": null, "audio": null, "function_call": null, "tool_calls": [], "reasoning": null }, "logprobs": null, "finish_reason": "length", "stop_reason": null, "token_ids": null } ], "service_tier": null, "system_fingerprint": null, "usage": { "prompt_tokens": 20, "total_tokens": 1044, "completion_tokens": 1024, "prompt_tokens_details": null }, "prompt_logprobs": null, "prompt_token_ids": null, "kv_transfer_params": null } ``` DeepSeek 官方的响应: ```json { "id": "647e1412-6dff-4719-bfb4-2ca9d4133da5", "object": "chat.completion", "created": 1774493587, "model": "deepseek-reasoner", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Hello! How can I assist you today? 😊", "reasoning_content": "Hmm, the user just said \"Hello!\" - a simple greeting. No complicated context or questions here. \n\nThis seems like the start of a casual conversation, so I should respond warmly to set a friendly tone. A cheerful greeting back would work, maybe add a touch of enthusiasm with an emoji. \n\nSince it's an opening message, I'll keep it open-ended by asking how I can help - that gives the user space to direct the conversation. No need for lengthy explanations here." }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 112, "total_tokens": 124, "prompt_tokens_details": { "cached_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 100 }, "prompt_cache_hit_tokens": 0, "prompt_cache_miss_tokens": 12 }, "system_fingerprint": "fp_eaab8d114b_prod0820_fp8_kvcache_new_kvcache" } ``` 模型有一个文件: ``` https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B/blob/main/tokenizer_config.json ``` 其中有一个设置: 采用 [Jinja — Jinja Documentation (3.1.x)](https://jinja.palletsprojects.com/en/stable/) 模板语法 ``` "chat_template": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set ns = namespace(is_first=false, is_tool=false, is_output_first=true, system_prompt='') %}{%- for message in messages %}{%- if message['role'] == 'system' %}{% set ns.system_prompt = message['content'] %}{%- endif %}{%- endfor %}{{bos_token}}{{ns.system_prompt}}{%- for message in messages %}{%- if message['role'] == 'user' %}{%- set ns.is_tool = false -%}{{'<|User|>' + message['content']}}{%- endif %}{%- if message['role'] == 'assistant' and message['content'] is none %}{%- set ns.is_tool = false -%}{%- for tool in message['tool_calls']%}{%- if not ns.is_first %}{{'<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{%- set ns.is_first = true -%}{%- else %}{{'\\n' + '<|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\\n' + '```json' + '\\n' + tool['function']['arguments'] + '\\n' + '```' + '<|tool▁call▁end|>'}}{{'<|tool▁calls▁end|><|end▁of▁sentence|>'}}{%- endif %}{%- endfor %}{%- endif %}{%- if message['role'] == 'assistant' and message['content'] is not none %}{%- if ns.is_tool %}{{'<|tool▁outputs▁end|>' + message['content'] + '<|end▁of▁sentence|>'}}{%- set ns.is_tool = false -%}{%- else %}{% set content = message['content'] %}{% if '' in content %}{% set content = content.split('')[-1] %}{% endif %}{{'<|Assistant|>' + content + '<|end▁of▁sentence|>'}}{%- endif %}{%- endif %}{%- if message['role'] == 'tool' %}{%- set ns.is_tool = true -%}{%- if ns.is_output_first %}{{'<|tool▁outputs▁begin|><|tool▁output▁begin|>' + message['content'] + '<|tool▁output▁end|>'}}{%- set ns.is_output_first = false %}{%- else %}{{'\\n<|tool▁output▁begin|>' + message['content'] + '<|tool▁output▁end|>'}}{%- endif %}{%- endif %}{%- endfor -%}{% if ns.is_tool %}{{'<|tool▁outputs▁end|>'}}{% endif %}{% if add_generation_prompt and not ns.is_tool %}{{'<|Assistant|>\\n'}}{% endif %}" } ``` # vLLM 为什么开头会没有 think 标签 具有比较参考性的讨论: ``` https://github.com/deepseek-ai/DeepSeek-R1/issues/352 ``` 原因就在于使用的推理引擎(比如 vLLM、SGLang、Ollama 等)在内部应用了刚刚讲过的那个 `chat_template`,并且启用了 `add_generation_prompt=True`。 还原一下推理引擎内部发生的事情: 1. **模板组装(Prompt 阶段)**: 当你的请求发给服务端时,引擎会将输入变成这样最后一段字符串: `…<|User|>请写一个快排<|Assistant|>\n` 2. **模型续写(Generation 阶段)**: 大语言模型的本质是“文本接龙”。当模型看到提示词**已经以 `\n` 结尾了**,它就会顺理成章地直接开始输出思考的内容: `"好的,我现在要帮助用户…"` 思考结束后,它自己输出一个 `\n\n`,接着输出最终代码。 3. **API 截断返回(Response 阶段)**: 推理引擎在返回 API 结果时,**只会返回模型“新生成”的文本**。因为开头的 `\n` 是由模板强制加在 Prompt 里的,属于“历史上下文”,所以它不会出现在生成的 `content` 里面。 这就导致了你看到的现象:输出字符串直接以思考内容开头,中后段有一个 `` 闭合标签。 在实际工程开发中,遇到这种标准情况,通常有以下几种处理方案: #### 方案 1:在前端/应用层手动补全(推荐) 既然我们知道只要启用了 R1 模型就一定会思考,那么在解析 API 返回时,直接手动给它加上 `\n` 即可。 ```python response_content = response['choices'][0]['message']['content'] # 如果内容里有 ,说明前半截是思考过程 if "" in response_content: # 补全开头的 标签,方便前端 Markdown 渲染或后续正则解析 full_content = "\n" + response_content else: full_content = response_content ``` #### 方案 2:将思考过程和正式回复分离 现代很多大模型 UI(比如 ChatBot)会把思考过程折叠起来。你可以直接用 `` 作为分割符,把字符串拆开: ```python parts = response_content.split("") if len(parts) == 2: reasoning_text = parts[0].strip() # 纯思考内容 final_answer = parts[1].strip() # 最终回答 else: reasoning_text = "" final_answer = response_content ``` #### 方案 3:使用支持 `reasoning_content` 的高级 API 推理框架 你提供的 JSON 结构中,其实有一个字段叫 `"reasoning": null`(在某些兼容 OpenAI 格式的框架如 vLLM 中叫 `reasoning_content`)。 如果你使用的推理引擎版本较新(配置正确的话),引擎会自动帮你把思考内容剥离出来放到 `reasoning` 字段中,而 `content` 字段里只保留干净的最终答案。你现在它还是 `null`,说明当前的引擎/版本只是把完整字符串粗暴地全塞进了 `content` 里。