Files
notes/resource/ai/大模型安装笔记/关于模型思考问题.md
T
Docker7530 5b32d919f3 1774877281
2026-03-30 21:28:03 +08:00

217 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
使用 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
}
```
如果不指定则在内容中,但目前有个问题返回的标签的不完整`</think>`
```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</think>\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 '</think>' in content %}{% set content = content.split('</think>')[-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><think>\\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><think>\n`
2. **模型续写(Generation 阶段)**
大语言模型的本质是“文本接龙”。当模型看到提示词**已经以 `<think>\n` 结尾了**,它就会顺理成章地直接开始输出思考的内容:
`"好的,我现在要帮助用户…"`
思考结束后,它自己输出一个 `</think>\n\n`,接着输出最终代码。
3. **API 截断返回(Response 阶段)**
推理引擎在返回 API 结果时,**只会返回模型“新生成”的文本**。因为开头的 `<think>\n` 是由模板强制加在 Prompt 里的,属于“历史上下文”,所以它不会出现在生成的 `content` 里面。
这就导致了你看到的现象:输出字符串直接以思考内容开头,中后段有一个 `</think>` 闭合标签。
在实际工程开发中,遇到这种标准情况,通常有以下几种处理方案:
#### 方案 1:在前端/应用层手动补全(推荐)
既然我们知道只要启用了 R1 模型就一定会思考,那么在解析 API 返回时,直接手动给它加上 `<think>\n` 即可。
```python
response_content = response['choices'][0]['message']['content']
# 如果内容里有 </think>,说明前半截是思考过程
if "</think>" in response_content:
# 补全开头的 <think> 标签,方便前端 Markdown 渲染或后续正则解析
full_content = "<think>\n" + response_content
else:
full_content = response_content
```
#### 方案 2:将思考过程和正式回复分离
现代很多大模型 UI(比如 ChatBot)会把思考过程折叠起来。你可以直接用 `</think>` 作为分割符,把字符串拆开:
```python
parts = response_content.split("</think>")
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` 里。
# 如何关闭本地部署r1的思考过程
[如何关闭本地部署r1的思考过程 · 议题 #512 · deepseek-ai/DeepSeek-R1](https://github.com/deepseek-ai/DeepSeek-R1/issues/512)