Ollama API & DeepSeek API 综合利用
小道消息知道学校拿ollama跑满血deepseek r1和v3,但是学校没有做ui,ds官方api也不能完全用到ollama上,于是自己探索了下ollama api文档,初步实现多轮交互、历史记录查询
环境
工具:python+ide,一个能发各种请求的工具(这里用的 Insomnia )
核心python库:openai
1 | pip3 install openai |
deepseek 兼容 openai,ollama 也兼容 openai,所以用 openai 发请求比直接发 post 方便很多
创建客户端
1 | from openai import OpenAI |
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
也会有回显)
可以看到这台机子跑了一个 deepseek-v3:latest
,参数 671b
(可选)查看模型信息
发送 post 到 /api/show
body填入:
1 | { |
会得到非常长的一个响应,模型主要信息在下面
简单对话测试
确定好模型信息,就可以向 /api/chat
发一个简单的对话测试了。可以通过post发送,也可以python发送
POST
1 | { |
model
填入目标模型名(必填)
messages
为发送的内容
-
role
为角色。system
可以直接理解为人设,user
可以直接理解为用户。这里的写法和 ollama 的 Modelfile 很相似。 -
content
代表内容
stream
为是否启用字节流。启用字节流的效果为将输出分为多个数据块,也就是ai边想边输出。不启用则为ai响应完毕后,将内容合并一整块返回
options
为一些附加选项,可以不写此项。这里写的 temperature
可以认为 “严肃程度”,这个值越高,得到的内容越丰富(发散)
还有一些选项,可以完全按照 ollama 的 Modelfile 填写
发送post请求响应可能会很久,但是Insomnia默认超过30s就是请求超时,所以需要调整一下最大等待时间
Insomnia右上角 Application
→ Preferences
进入 General
选项卡,下滑,将 Request timeout
改为0(即为不限制超时)
现在尝试发送请求
stream true:
stream false:
ai 可以正常回显,接下来尝试python发送请求(openai)
python
非流式
1 | from openai import OpenAI |
流式
和非流式的输出有所不同,需要添加一个循环,逐个打印字节流
1 | from openai import OpenAI |
这里的 chunk
是 openai 中的 “块”,参考上方 POST 发送流式请求,chunk代表每一个数据块。
choices
是 openai 响应中的第一个 choice
对象。choices
是一个列表,包含了包含增量更新的内容。
也许会疑惑,在上面的 post 请求中没有看到choices和delta,他们是从哪来的
实际上这是openai的问题,我们使用的是兼容openai的 /v1 端点,实际上,请求发送到了 /v1/chat/completions
为了进一步探究这个问题,我们可以向 /v1/chat/completions 发送 post
单独看一个数据块
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
}
]
}这样就很清楚了,这一整块就是所谓的
chunk
,choices[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?"}
]在第二轮请求时:
要将第一轮中模型的输出添加到
messages
末尾将新的提问添加到
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 | messages = [{ |
进入Q&A循环
1 | while True: |
这里用的是流式对话,参考上方流式对话示例,定义一个新的函数 print_messages()
用于打印流式响应内容
1 | def print_messages(response): |
为了方便,将流式相应内容收集为一整个内容,在 print_messages()
内添加一个 assistant_reply
变量,每获取到新的字节流就追加,这样获得完整响应
1 | def print_messages(response): |
这样继续修改程序,在循环中调用 print_messages
函数
1 | assistant_reply = print_messages(response) # 打印对话 |
在循环最后追加 ai content
1 | messages.append({"role": "assistant", "content": assistant_reply}) |
完整程序
1 | from openai import OpenAI |
实际上,上下文效果并不好,可能需要更严格的设定,但目前也勉强算有上下文衔接了