0%

使用过 OpenAI 或者其他 LLM 的人应该都知道,有时候 LLM 会回答得不那么准确。 这是因为,LLM 可能并不知道你问题的背景是什么,所以只能从它大量学习到的数据中,找到一个最接近的答案, 但可能这个答案跟我们实际想要的答案相去甚远。

指定角色

如果我们为 LLM 指定一个角色,那么就等于给了 LLM 一个明确的指示,为它提供了一个上下文框架,这样它就能使用相关的知识来回答问题。

下面是一些例子:

指定为翻译

[翻译]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage

chat = ChatOpenAI(
model="yi-large",
temperature=0.3,
max_tokens=200,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

messages = [
SystemMessage(content="你是一名翻译,把用户的输入翻译为英语"),
HumanMessage(content="今天天气真好"),
]

response = chat.invoke(messages)

print(response.content)

指定为 程序员

[程序员]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage

chat = ChatOpenAI(
model="yi-large",
temperature=0.3,
max_tokens=200,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

messages = [
# 如果设定的是翻译角色,那么 LLM 只是翻译 ruby 是什么
# SystemMessage(content="你是一名翻译"),
SystemMessage(content="你是一名程序员"),
HumanMessage(content="ruby是什么"),
]

response = chat.invoke(messages)

print(response.content)

在上面的例子中,我们可以看到,在给 LLM 设定的角色是翻译的时候,我问他它 ruby 是什么,它直接翻译成中文给我。 但是当我给它设定的角色是程序员的时候,它就给我回答了 ruby 是一种编程语言。

指定上下文

下面这个例子中,我们为 LLM 提供了一个上下文,这样 LLM 就能更好地回答我们的问题。

如果我们直接问 LLM 我想查询所有年龄大于 18 岁的用户,应该怎么写 SQL 语句? 的话,它可能会回答得不那么准确,因为它并不知道我们的表结构是怎样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage

chat = ChatOpenAI(
model="yi-large",
temperature=0.3,
max_tokens=200,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

messages = [
SystemMessage(content="你是一名 MySQL DBA"),
HumanMessage(content=""""
假设我有一个 user 表,里面有 id, name, age 三个字段,我想查询所有年龄大于 18 岁的用户,应该怎么写 SQL 语句?
"""),
]

response = chat.invoke(messages)

print(response.content)

总结

如果我们想从 LLM 那里得到更加精确、高质量的答案,我们就需要为它指定一个角色,或者给他提供更多跟我们问题相关的内容。

当然,我们没办法给他提供太多的上下文,因为这样会让 LLM 处理很久,需要更昂贵的价格,同时,每个 LLM 的最大输入长度也是有限制的。

在开始之前,我们需要先准备一个可以调用 OpenAPI 的 key,又或者是其他 LLM 的 key。

因为墙的原因,所以没有办法直接使用 OpenAI 的 key 来调用,但是我们可以使用一些替代品或者代理,可选的有:

依赖

我们可以使用 openai 的 SDK 来调用它们的接口,同时也可以使用 langchain 这个库来调用它们的接口。

不过本系列文章都会基于 langchain 来进行实现。langchain 是一个基于 OpenAPI 的封装,可以方便的调用 OpenAPI 的接口。

同时很多国内的大模型也支持通过 langchain 的 API 来使用,非常方便。

通过 OpenAI SDK 使用

目前市面上大多数的 LLM 都支持 OpenAI 的接口,所以我们可以通过 OpenAI 的 SDK 来调用这些接口。

需要注意的是:

  1. 模型:不同的 LLM 有不同的模型,比如零一万物的模型是 yi-large,OpenAI 的模型是 gpt-3.5-turbo,智谱清言的模型是 glm-4
  2. base_url:不同的 LLM 有不同的 base_url,需要根据不同的 LLM 来设置。
  3. api_key:不同的 LLM 有不同的 api_key

下面是一个使用 OpenAI SDK 的例子:

依赖安装:

1
pip install openai

零一万物

[零一万物]
1
2
3
4
5
6
7
8
9
10
11
12
from openai import OpenAI

client = OpenAI(
api_key='your key',
base_url='https://api.lingyiwanwu.com/v1'
)
completion = client.chat.completions.create(
model="yi-large",
messages=[{"role": "user", "content": "Hi, who are you?"}]
)

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

OpenAI HK

[OpenAI HK]
1
2
3
4
5
6
7
8
9
10
11
12
from openai import OpenAI

client = OpenAI(
api_key='your key',
base_url="https://api.openai-hk.com/v1"
)
completion = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "Hi, who are you?"}]
)

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

智谱清言

[智谱清言]
1
2
3
4
5
6
7
8
9
10
11
12
from openai import OpenAI

client = OpenAI(
api_key='your key',
base_url='https://open.bigmodel.cn/api/paas/v4/'
)
completion = client.chat.completions.create(
model="glm-4",
messages=[{"role": "user", "content": "Hi, who are you?"}]
)

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

通过 langchain 使用

依赖安装:

1
pip install langchain_openai

跟上面的例子差不多,修改一下 base_urlmodel 以及 api_key 即可。

零一万物

[零一万物]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage

chat = ChatOpenAI(
model="yi-large",
temperature=0.3,
max_tokens=200,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1"
)

messages = [
SystemMessage(content="你是一名精通了 golang 的专家"),
HumanMessage(content="写一个 golang 的 hello world 程序"),
]

response = chat.invoke(messages)

print(response.content)

OpenAI HK

[OpenAI HK]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage

chat = ChatOpenAI(
model="gpt-3.5-turbo",
temperature=0.3,
max_tokens=200,
api_key="your key",
base_url="https://api.openai-hk.com/v1"
)

messages = [
SystemMessage(content="你是一名精通了 golang 的专家"),
HumanMessage(content="写一个 golang 的 hello world 程序"),
]

response = chat.invoke(messages)

print(response.content)

智谱清言

[智谱清言]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage

chat = ChatOpenAI(
model="glm-4",
temperature=0.3,
max_tokens=200,
api_key="your key",
base_url='https://open.bigmodel.cn/api/paas/v4/'
)

messages = [
SystemMessage(content="你是一名精通了 golang 的专家"),
HumanMessage(content="写一个 golang 的 hello world 程序"),
]

response = chat.invoke(messages)

print(response.content)

环境变量指定 API KEY 以及 BASE URL

我们可以通过环境变量来指定 api_key 以及 base_url,这样我们就不需要在代码中指定了。

1
2
export OPENAI_API_KEY="your key"
export OPENAI_BASE_URL="https://api.lingyiwanwu.com/v1"

这样我们就可以直接使用 langchain 来调用了,当然,传递给 ChatOpenAImodel 参数还是得指定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage

chat = ChatOpenAI(
model="yi-large",
temperature=0.3,
max_tokens=200
)

messages = [
SystemMessage(content="你是一名精通了 golang 的专家"),
HumanMessage(content="写一个 golang 的 hello world 程序"),
]

response = chat.invoke(messages)

print(response.content)

注意事项

  1. 截止到目前为止(2024-07-15),langchain 还在一个很频繁的更新阶段,所以可能会有一些 API 的变动,所以本文的一些 langchain 示例代码可能在未来会不可用。

本文中,我们会通过一个简单的例子来展示如何使用 langchain 来调用大模型的 chat API(使用 Chat Model)。 这个例子前面也有使用过,但是前面还没有针对里面的内容进行详细的说明。

配置 key 的文档请看 langchain 入门指南(一)- 准备 API KEY

依赖安装

1
pip install -U langchain-openai

示例

下面的 ChatOpenAI 表示我们要使用的是 Chat Model,顾名思义,这个模型是用来进行对话的,这也是我们最常用的一种模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage

chat = ChatOpenAI(
model="yi-large",
temperature=0.3,
max_tokens=200,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1"
)

messages = [
SystemMessage(content="你是一名精通了 golang 的专家"),
HumanMessage(content="写一个 golang 的 hello world 程序"),
]

response = chat.invoke(messages)

print(response.content)

ChatOpenAI 说明

使用 ChatOpenAI 类,我们可以调用 chat API。ChatOpenAI 类的构造函数有以下参数:

ChatOpenAI 参数

  • model:模型名称,例如 yi-large(零一万物),gpt-3.5-turbo(OpenAI HK)等。
  • temperature:用于控制生成文本的多样性,值越大,生成的文本越多样化。
  • max_tokens:生成文本的最大长度。(我们的输入和 LLM 的输出都需要消耗 token,所以如果只是测试,可以控制一下输出的 token 数量)
  • api_key:API 密钥(支持多种,不只是 OpenAI 的)。不填写的话,会从环境变量中读取(对应的环境变量是 OPENAI_API_KEY)。
  • base_url:API 的接口地址。不填写的话,会从环境变量中读取(对应的环境变量是 OPENAI_BASE_URL)。
  • timeout:超时时间,单位是秒。
  • max_retries: 最大重试次数。

invoke 方法的参数说明

我们可以看到上面的例子传递了一个 messages 参数,这个参数是一个列表,里面包含了 HumanMessageSystemMessage

在其他地方,我们可能会看到其他形式的参数,它实际上也支持很多种形式,例如:

元组列表

[元组列表]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from langchain_openai import ChatOpenAI

chat = ChatOpenAI(
model="yi-large",
temperature=0.3,
max_tokens=200,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

messages = [
('system', '你是一名精通了 golang 的专家'),
('human', '写一个 golang 的 hello world 程序')
]

response = chat.invoke(messages)

print(response.content)

BaseMessage 列表

[BaseMessage 列表]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage

chat = ChatOpenAI(
model="yi-large",
temperature=0.3,
max_tokens=200,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

messages = [
SystemMessage(content="你是一名精通了 golang 的专家"),
HumanMessage(content="写一个 golang 的 hello world 程序"),
]

response = chat.invoke(messages)

print(response.content)

字符串

[字符串]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from langchain_openai import ChatOpenAI

chat = ChatOpenAI(
model="yi-large",
temperature=0.3,
max_tokens=200,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

# 这个字符串参数会被转换为 HumanMessage
response = chat.invoke('使用 golang 写一个 hello world 程序')

print(response.content)

字符串列表

[字符串列表]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from langchain_openai import ChatOpenAI

chat = ChatOpenAI(
model="yi-large",
temperature=0.3,
max_tokens=200,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

messages = [
"你是一名精通了 golang 的专家",
"写一个 golang 的 hello world 程序",
]

response = chat.invoke(messages)

print(response.content)

invoke 方法的返回值

上面是直接打印了返回值的 content 属性,实际上返回值中包含了其他一些有用的信息:

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
{
"lc": 1,
"type": "constructor",
"id": [
"langchain",
"schema",
"messages",
"AIMessage"
],
"kwargs": {
"content": "<...省略...>",
"response_metadata": {
"token_usage": {
"completion_tokens": 200,
"prompt_tokens": 35,
"total_tokens": 235
},
"model_name": "yi-large",
"system_fingerprint": null,
"finish_reason": "length",
"logprobs": null
},
"type": "ai",
"id": "run-29131a4f-e792-4c9e-8cf5-490afed94176-0",
"usage_metadata": {
"input_tokens": 35,
"output_tokens": 200,
"total_tokens": 235
},
"tool_calls": [],
"invalid_tool_calls": []
}
}

一些字段说明:

  • completion_tokens/output_tokens 是生成的文本的 token 数量。
  • prompt_tokens/input_tokens 是输入的 token 数量。
  • total_tokens 是单次请求总的 token 数量。

在实际的应用中,我们需要注意使用的 token 的数量,防止消耗太多的 token,因为 token 是要花钱来购买的。 如果我们是为其他人提供服务,可能就需要针对不同的用户来统计 token 的使用情况,以便计费。

很多时候我们会使用 golang 来作为 web 服务的后端,这时候我们就需要对我们的 http 接口进行测试。Go 语言的标准库提供了一个非常方便的测试工具:net/http/httptest 包,可以用来测试 http 服务。 比如对自己写的 http 接口进行测试,通过 mock 掉 ResponseWriter;又或者在单元测试中 mock 掉对外部的 http 请求,让我们的测试不依赖于外部的 http 服务。

http 测试的作用

  1. 测试你写的 http 接口:具体来说,就是测试你的 http handler 是否按照预期工作。比如你的 handler 是否正确的处理了请求参数,是否正确的返回了响应。
  2. mock 掉对外部的 http 请求:在单元测试中,我们通常不希望依赖于外部的 http 服务,因为外部的服务可能会有变化,这样会导致我们的测试不稳定。

如何测试你的 http 接口

假设我们有一个返回 Hello, world! 的 http 接口:

1
2
3
4
5
6
7
package main

import "net/http"

func HelloHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
}

我们单元测试中,可能会想知道,自己构建一个 http.Request 得到的响应是否是我们预期的。比如我们想知道,是否返回了 Hello, World!,状态码是否是 200 等。

这个时候,我们就可以使用 httptest.NewRecorder 来模拟一个 http.ResponseWriter,它跟 http.ResponseWriter 的行为一样, 但是它不会真的发送响应到客户端,而是把响应保存在内存中,这样我们就可以方便的测试我们的 handler 是否按照预期工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func TestHelloHandler(t *testing.T) {
// 创建一个请求
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
// 创建一个响应
w := httptest.NewRecorder()
// 调用我们的处理函数
HelloHandler(w, req)

// 检查响应码
if w.Code != http.StatusOK {
t.Errorf("响应码错误: %d", w.Code)
}
// 检查响应体
if w.Body.String() != "Hello, World!" {
t.Errorf("响应体错误: %q", w.Body.String())
}
}

说明:

  • 我们使用 httptest.NewRequest 来创建一个请求,这个请求可以用来模拟一个 http 请求。
  • 我们使用 httptest.NewRecorder 来创建一个响应,这个响应可以用来模拟一个 http.ResponseWriter
  • 调用 HelloHandler 的过程实际上等同于我们的 http 服务接收到了一个请求,然后返回了一个响应。
  • 最后我们检查了响应码和响应体是否符合预期。

httptest.NewRecorder 返回了一个 ResponseRecorder 实例,它有如下字段:

  • Code 记录了响应的状态码
  • Body 记录了响应的 body
  • HeaderMap 记录了响应的 header
  • Flushed 记录了响应是否已经被发送

它具有如下方法:

  • Header() 返回响应的 header
  • Result() 返回响应的 http.Response 实例
  • Write([]byte) 写入响应的 body(它的调用发生在我们的 http handler 中)
  • WriteString(string) 写入响应的 body(跟 Write([]byte) 类似,只是参数类型不一样)
  • WriteHeader(int) 写入响应的状态码
  • Flush() 将响应发送到客户端

总的来说,我们可以通过 httptest.NewRecorder 来模拟一个 http.ResponseWriter,然后调用我们的 handler,最后检查响应是否符合预期。 也就是说,我们不用启动一个真正的 http 客户端来对我们的接口进行测试;当然,我们也不需要真的启动一个真正的 http 服务。

mock 掉对外部的 http 请求

有时候,我们开发的功能会依赖于外部的 http 服务,比如调用一个第三方的接口。在单元测试中,我们通常不希望依赖于外部的服务,因为外部的服务可能会有变化,这样会导致我们的测试不稳定。

如何 mock ?

  1. 定义一个 http.Handler 的实现,然后在这个 handler 中返回我们预期的响应
  2. 使用 httptest.NewServer 来启动一个 mock http 服务,使用上面的 handler
  3. 使用 httptest.Server 实例的 URL 字段来获取这个 mock 服务的地址
  4. 最终,当我们访问这个 mock 服务的时候,它会返回我们预期的响应

示例

先定义一个 http.Handler,我们判断请求的路径是 /hello 的时候,返回 Hello, World!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "net/http"

type MyHandler struct{}

func (m *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/hello":
w.Write([]byte("Hello, World!"))
default:
http.Error(w, "Not Found", http.StatusNotFound)
}
}

接着,启动一个 httptest.Server,并指定我们的 HandlerFunc

1
2
3
4
server := httptest.NewServer(&MyHandler{})
defer server.Close()

println(server.URL)

这里获取到的 server.URL 就是我们的 mock 服务的地址,我们可以通过这个地址来访问我们的 mock 服务。

我们可以将自己代码中访问外部服务的地址替换为 server.URL,这样我们的测试就不会依赖于外部服务,而是依赖于我们自己的 mock 服务。这样就可以实现对外部服务的 mock。

下面是一个完整的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// myFunc 请求 url 并返回响应
func myFunc(url string) string {
resp, err := http.Get(url)
if err != nil {
return err.Error()
}
defer resp.Body.Close()

res, _ := io.ReadAll(resp.Body)
return string(res)
}

func TestHello(t *testing.T) {
server := httptest.NewServer(&MyHandler{})
defer server.Close()

res := myFunc(server.URL + "/hello")
if res != "Hello, World!" {
t.Errorf("Expected 'Hello, World!', got %s", res)
}
}

上面这个例子中:

  1. 我们定义了一个 myFunc 函数,它会请求一个 url 并返回响应
  2. 我们定义了一个 TestHello 测试函数,它会启动一个 mock 服务,然后调用 myFunc 函数,最后检查响应是否符合预期

它实现的功能是:我们的 myFunc 函数会请求 mock 服务的 /hello 路径,然后返回响应。 在实际开发中,myFunc 接收的 url 可能是一个外部服务的地址,比如第三方 api 的地址。

有了 httptest,现在我们只需要替换 myFunc 中的 url 为 server.URL,就可以实现对外部服务的 mock。

总结

在本文中,我们深入探讨了如何使用 Go 语言的 net/http/httptest 包来测试 HTTP 服务。我们首先介绍了 HTTP 测试的作用,包括测试自定义的 HTTP 接口以及模拟外部 HTTP 请求,以确保单元测试的稳定性和可控性。

接着,我们详细介绍了如何测试自定义的 HTTP 接口,通过使用 httptest.NewRecorder 来模拟 http.ResponseWriter,从而捕获 HTTP 处理函数的输出。我们还展示了如何检查响应码和响应体,以验证处理函数的行为是否符合预期。

然后,我们讨论了如何模拟外部 HTTP 请求,通过定义一个 http.Handler 并使用 httptest.NewServer 来创建一个 mock HTTP 服务器。我们看到,通过这种方式,我们可以控制外部服务的响应,从而在测试环境中隔离外部依赖。

下载

https://www.anaconda.com/download

使用

1. 查看当前环境列表

1
conda env list

输出:

1
2
3
4
# conda environments:
#
base /opt/anaconda3
gyp /opt/anaconda3/envs/gyp

2. 创建新环境

1
conda create --name myenv python=3.6

myenv 为环境名称,python=3.6 为指定 Python 版本

3. 激活环境

1
conda activate myenv

4. 退出环境

1
conda deactivate

5. 安装对应依赖库

1
pip install 依赖名

或者:

1
conda install 依赖名

6. 删除环境

1
conda remove --name myenv

仅删除环境里面的一个依赖:

1
conda remove --name myenv 依赖名

7. 查看 conda 版本

1
conda --version

8. 查看当前 conda 环境安装的所有依赖包

1
conda list

其他

1. 导出 conda 环境

1
conda list --explicit > /tmp/export.txt

2. 关闭 terminal 时自动激活环境

1
conda config --set auto_activate_base false

3. 修改镜像源

1
2
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
conda config --set show_channel_urls yes

4. 查看当前源命令

1
2
conda config --show channels 
conda config --get channels

5. 修改成默认源

1
conda config --remove-key channels

删除某镜像源:

1
conda config --remove channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/

6. 离线安装 python 依赖

1
conda install /path/to/package.tar.bz2

可以在 https://anaconda.org/ 上下载对应的依赖包。