Ollama API & DeepSeek API 综合利用

Ollama API & DeepSeek API 综合利用

小道消息知道学校拿ollama跑满血deepseek r1和v3,但是学校没有做ui,ds官方api也不能完全用到ollama上,于是自己探索了下ollama api文档,初步实现多轮交互、历史记录查询

deepseek api文档

ollama api文档中文翻译

环境

工具:python+ide,一个能发各种请求的工具(这里用的 Insomnia )

核心python库:openai

1
pip3 install openai

deepseek 兼容 openai,ollama 也兼容 openai,所以用 openai 发请求比直接发 post 方便很多

创建客户端

1
2
3
from openai import OpenAI

client = OpenAI(api_key="666", base_url="http://xx.xx.xx.xx:11434/v1")

api_key 是令牌内容,本地部署的 ollama 一般没有令牌限制,所以可以随意填

base_url 是目标ollama所在服务器ip,11434 为ollama默认端口,/v1 是根据deepseek api描述,用来兼容openai。原文如下

出于与 OpenAI 兼容考虑,您也可以将 base_url 设置为 https://api.deepseek.com/v1 来使用,但注意,此处 v1 与模型版本无关。

确定模型信息

列出本地模型

1
GET /api/tags

显示模型信息

1
POST /api/show

首先 GET 查看有哪些模型(这一步可以不用请求工具,直接访问 /api/tags 也会有回显)

image-20250217195231884

可以看到这台机子跑了一个 deepseek-v3:latest,参数 671b

(可选)查看模型信息

发送 post 到 /api/show

body填入:

1
2
3
{
"model": "要查询的模型"
}

会得到非常长的一个响应,模型主要信息在下面

image-20250217195510361

简单对话测试

确定好模型信息,就可以向 /api/chat 发一个简单的对话测试了。可以通过post发送,也可以python发送

POST

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"model": "deepseek-v3:latest",
"messages": [
{
"role": "system",
"content": "你是一名高级Java程序员,擅长处理各种BUG以及编写高质量代码"
},
{
"role": "user",
"content": "请帮我使用Java写一个HelloWorld示例"
}
],
"stream": true,
"options": {
"temperature": 0.2
}
}

model 填入目标模型名(必填)

messages 为发送的内容

  • role 为角色。system 可以直接理解为人设,user 可以直接理解为用户。这里的写法和 ollama 的 Modelfile 很相似。

  • content 代表内容

stream 为是否启用字节流。启用字节流的效果为将输出分为多个数据块,也就是ai边想边输出。不启用则为ai响应完毕后,将内容合并一整块返回

options 为一些附加选项,可以不写此项。这里写的 temperature 可以认为 “严肃程度”,这个值越高,得到的内容越丰富(发散)

还有一些选项,可以完全按照 ollama 的 Modelfile 填写

发送post请求响应可能会很久,但是Insomnia默认超过30s就是请求超时,所以需要调整一下最大等待时间

Insomnia右上角 ApplicationPreferences 进入 General 选项卡,下滑,将 Request timeout 改为0(即为不限制超时)

image-20250217201332940

现在尝试发送请求

stream true:

image-20250217201511278

stream false:

image-20250217201653861

ai 可以正常回显,接下来尝试python发送请求(openai)

python

非流式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from openai import OpenAI

client = OpenAI(api_key="666", base_url="http://xx.xx.xx.xx:11434/v1")

response = client.chat.completions.create(
model="deepseek-v3:latest",
messages=[
{"role": "system", "content": "You are a helpful assistant"},
{"role": "user", "content": "Hello"},
],
stream=False
)

print(response.choices[0].message.content)

image-20250217202528348

流式

和非流式的输出有所不同,需要添加一个循环,逐个打印字节流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from openai import OpenAI

client = OpenAI(api_key="666", base_url="http://xx.xx.xx.xx:11434/v1")

response = client.chat.completions.create(
model="deepseek-v3:latest",
messages=[
{"role": "system", "content": "You are a helpful assistant"},
{"role": "user", "content": "Hello"},
],
stream=True
)

for chunk in response:
print(chunk.choices[0].delta.content, end='', flush=True)

这里的 chunk 是 openai 中的 “块”,参考上方 POST 发送流式请求,chunk代表每一个数据块。

choices是 openai 响应中的第一个 choice 对象。choices是一个列表,包含了包含增量更新的内容。

也许会疑惑,在上面的 post 请求中没有看到choices和delta,他们是从哪来的

实际上这是openai的问题,我们使用的是兼容openai的 /v1 端点,实际上,请求发送到了 /v1/chat/completions

为了进一步探究这个问题,我们可以向 /v1/chat/completions 发送 post

image-20250217203822560

单独看一个数据块

1
data: {"id":"chatcmpl-977","object":"chat.completion.chunk","created":1739795748,"model":"deepseek-v3:latest","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"当然"},"finish_reason":null}]}

稍微修正一下(去掉data:后json格式化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"id": "chatcmpl-977",
"object": "chat.completion.chunk",
"created": 1739795748,
"model": "deepseek-v3:latest",
"system_fingerprint": "fp_ollama",
"choices": [
{
"index": 0,
"delta": {
"role": "assistant",
"content": "当然"
},
"finish_reason": null
}
]
}

这样就很清楚了,这一整块就是所谓的 chunkchoices[0] 指的是第一组花括号的内容,delta.content就是这个字节流的内容。

于是,chunk.choices[0].delta.content 就可以访问到这个字节流的内容。由于print默认结尾为回车,需要指定为空字符来避免每个字节流都换行。最后的flush用来刷新缓冲区。

也许会有怀疑,这是真流式还是假流式,是真的ai一边想一边输出,还是主动将输出分割成一个个小块然后伪造流式输出的样子。其实可以比较输出时间,非流式输出响应时间很长很长,而流式输出响应很快。这里不做演示,可以自行尝试。

扩展上下文

多尝试几次简单对话可以发现,ai本身没有记忆上下文,每次都是新的对话

deepseek api文档对此也有介绍:

DeepSeek /chat/completions API 是一个“无状态” API,即服务端不记录用户请求的上下文,用户在每次请求时,需将之前所有对话历史拼接好后,传递给对话 API。

文档中保持上下文的示例:

第一轮请求时,传递给 API 的 messages 为:

1
2
3
[
{"role": "user", "content": "What's the highest mountain in the world?"}
]

第二轮请求时:

  1. 要将第一轮中模型的输出添加到 messages 末尾

  2. 将新的提问添加到 messages 末尾

最终传递给 API 的 messages 为:

1
2
3
4
5
[
{"role": "user", "content": "What's the highest mountain in the world?"},
{"role": "assistant", "content": "The highest mountain in the world is Mount Everest."},
{"role": "user", "content": "What is the second?"}
]

所以需要做的就是,记录每一次输入和输出作为 old_message,追加新的 user content,构成一个新的 message 发送给ai,记录 ai content,追加到 message 尾部,如此循环。

先定义一个初始message

1
2
3
4
messages = [{
"role": "system",
"content": "你需严格跟踪对话历史(位于[系统消息]之后),始终保持对话连贯性。响应时需主动关联先前讨论内容。"
}]

进入Q&A循环

1
2
3
4
5
6
7
8
9
10
11
12
13
while True:
user_input = input("##user##: ")

messages.append({"role": "user", "content": user_input})

response = client.chat.completions.create( # 生成对话
model="deepseek-v3:latest",
messages=messages,
stream=True # 流式对话
)
# 打印对话
# 添加对话记录

这里用的是流式对话,参考上方流式对话示例,定义一个新的函数 print_messages() 用于打印流式响应内容

1
2
3
4
5
def print_messages(response):
print("##ds##: ", end='')
for chunk in response:
print(chunk.choices[0].delta.content, end='', flush=True)
print()

为了方便,将流式相应内容收集为一整个内容,在 print_messages() 内添加一个 assistant_reply 变量,每获取到新的字节流就追加,这样获得完整响应

1
2
3
4
5
6
7
8
def print_messages(response):
print("##ds##: ", end='')
assistant_reply = "" # 收集各个chunk的回复
for chunk in response:
print(chunk.choices[0].delta.content, end='', flush=True)
assistant_reply += chunk.choices[0].delta.content
print()
return assistant_reply

这样继续修改程序,在循环中调用 print_messages 函数

1
assistant_reply = print_messages(response) # 打印对话

在循环最后追加 ai content

1
messages.append({"role": "assistant", "content": assistant_reply})

完整程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from openai import OpenAI

client = OpenAI(api_key="666", base_url="http://xx.xx.xx.xx:11434/v1") # 创建客户端

def print_history(messages): # 打印对话记录
print("\n--------------------history--------------------")
for index, message in enumerate(messages):
print(f"##{index}##: {message['role']}: {message['content']}")
print("-----------------------------------------------\n")

def print_messages(response):
print("##ds##: ", end='')
assistant_reply = "" # 收集各个chunk的回复
for chunk in response:
print(chunk.choices[0].delta.content, end='', flush=True)
assistant_reply += chunk.choices[0].delta.content
print()
return assistant_reply

messages = [{
"role": "system",
"content": "你需严格跟踪对话历史(位于[系统消息]之后),始终保持对话连贯性。响应时需主动关联先前讨论内容。"
}]

while True:
user_input = input("##user##: ")
if user_input.lower() == "q": # 按q退出
break
if user_input.lower() == "h": # 按h打印对话记录
print_history(messages)
continue

messages.append({"role": "user", "content": user_input})

response = client.chat.completions.create( # 生成对话
model="deepseek-v3:latest",
messages=messages,
stream=True # 流式对话
)

assistant_reply = print_messages(response) # 打印对话
messages.append({"role": "assistant", "content": assistant_reply})

image-20250217222357183

实际上,上下文效果并不好,可能需要更严格的设定,但目前也勉强算有上下文衔接了