LLM 上层设施 基于 Langchain 复现 Baseline

Baseline的代码是一个简单的补全解法,没有太多新东西,那来复习下 Langchain 调 API 吧。现在已经是 LCEL(LangChain Expression Language) 的时代了,就只用新写法了。

先从基础的 API 讲起。

OpenAI API 的标准是日前大部分模型服务商都在遵循的,这里我们就以OpenAI Chat API为例来讲解。本次代码中,dashscope 也提供的是OpenAI Chat API风格的接口。

OpenAI Chat API Request Body

OpenAI 官方的接口 Endpoint 是 POST https://api.openai.com/v1/chat/completions,带有认证头 Authorization ,认证头是 Bearer 格式,token是 API Key。

下文中是所有的字段,但是常用的只有 messagesmodel,其次是生成参数。回复常用的也只有 choices

messages Array<Message> Required

Messages 字段是一个数组,每个元素是一个 message 对象。

Message 对象有 4 种,分别是 System, User, Assistant, ToolFunction 类 message 已经被废弃。

System Message: Message

系统消息是由系统生成的消息,不需要用户提供。一般用于基本的 prompt 工程。

  • role: 总是 system,Required
  • content: 系统消息内容,Required
  • name: 参与者的名字,Optional

这里的参与者名字是指这个消息的来源,用于在多用户的情况下区分不同用户的消息,它不太常用。

例如,一个系统消息可以是:

{
  "role": "system",
  "content": "Hello, how can I help you?",
  "name": "developer"
}

User Message: Message

用户消息是由用户提供的消息,需要用户提供。一般用于用户输入。

  • role: 总是 user,Required
  • content: 用户消息内容,Required
  • name: 参与者的名字,Optional

与系统消息唯一的区别是 role 字段。

例如,一个用户消息可以是:

{
  "role": "user",
  "content": "What's the weather like today?",
  "name": "Alice"
}

Assistant Message: Message

助手消息是由助手生成的消息,不需要用户提供。一般用于助手回复。

助手是指的语言模型能调用的其它一些工具,例如知识库、搜索引擎等。

  • role: 总是 assistant,Required
  • content: 助手消息内容,Optional,因为有 tool calls 作为 content 的实际内容。如果 tool calls 为空,则 content 必须提供。
  • tool_calls: 函数调用,它也是一个列表,每个对象包含如下的字段,Optional,但如果 content 为空,则 tool calls 必须提供。

    • name: 函数名,Required
    • arguments: 函数参数,是一个对象,Required
    • id: 函数调用的唯一标识,Required

例如,一个助手消息可以是:

{
    "role": "assistant",
    "name": "AssistantBot",
    "tool_calls": [
        {
            "id": "123456",
            "type": "function",
            "function": {
                "name": "get_weather",
                "arguments": {
                    "location": "New York"
                }
            }
        }
    ]
}

Tool Message: Message

工具消息是助手消息的返回结果。

  • role: 总是 tool,Required
  • content: 工具消息内容,Required
  • tool_call_id: 对应的助手消息的 tool_callid,Required

例如,一个工具消息可以是:

{
    "role": "tool",
    "content": "Sunny",
    "tool_call_id": "123456"
}

model String Required

模型名称,例如 gpt-4o

frequency_penalty Float Optional

默认是 0,取值在 -2 到 2 之间。正值会给重复的词语更高的惩罚。

logit_bias Map<Integer, Float> Optional

默认是空,是一个整数到浮点数的映射。整数是 token 的索引,浮点数是对应的 logit 偏置。

logprobs Boolean Optional

默认为 false。如果为 true,返回的结果会包含 logprobs 字段,这个字段包含了每个 token 的 logprobs。

max_tokens Integer Optional

返回的结果最多包含的 token 数。

n Integer Optional

返回得到的结果的数量,默认为 1。

presence_penalty Float Optional

默认是 0,取值在 -2 到 2 之间。正值会给出现的词语更高的惩罚。

具体 presence_penalty 和 frequency_penalty 的区别可以参考 OpenAI 文档

response_format Object Optional

必须是 { "type": "json" }{ "type": "text" }。默认为后者。使用 json 会强制模型返回一个 json 格式的 message,使用 text 会返回一个文本格式的结果。

seed Integer Optional

随机种子,无需解释。

service_tire String Optional

服务等级可用defaultauto,默认为auto

如果设置了服务等级,返回结果也会包含服务等级的字段。

stop Array<String> | String Optional

停止词,如果返回结果中包含了停止词,会提前结束。

stream Boolean Optional

如果为 true,会使用流模式返回。

stream_options Object Optional

只有一个字段,include_usage,如果为 true,返回时会包含一个 usage 字段。

对于其它的所有对象,usage 都是空。在 [DONE] 信息被返回前,会多发一个对象。这个对象的返回信息是空,但是 usage 字段会包含一些信息。

temperature Float Optional

默认是 1,取在 0 到 2 之间。

top_p Float Optional

默认是 1,取在 0 到 1 之间。

tools Array<Tool> Optional

支持的工具列表,每个工具是一个对象。

工具对象包含如下字段:

  • type: 工具类型,目前只支持 function,Required。
  • function: 函数对象,Required。

    • description: 函数描述,Optional。
    • name: 函数名,Required。
    • parameters: 函数参数,是一个对象,Optional。如果函数没有参数,可以为空对象。

      • name: 参数名,Required。
      • type: 参数类型,Required。
      • description: 参数描述,Optional。

tool_choice String | Object Optional

控制模型可用的工具。如果是字符串,可以为 none,禁止所有工具,auto,自动选择工具,required,必须使用工具。

使用对象时,每个对象都模型必须使用的工具。

  • type: 工具类型,目前只支持 function,Required。
  • function: 函数对象,Required。

    • name: 函数名,Required。

parallel_tool_calls Boolean Optional

使用并行工具调用。

user String Optional

用户 ID。

OpenAI Chat API Response Body

返回对象有两种,chat completion objectchat completion chunk object

Chat Completion Object

  • id: 请求的唯一标识。
  • created: 请求的时间,Unix 秒时间戳。
  • model: 模型名称。
  • service_tier: 服务等级。
  • system_fingerprint: 系统指纹,代表了调用时的系统环境。
  • object: 返回对象的名称,总是chat.chat.completion
  • usage: 包含一些计费信息。

    • total_tokens: 总 token 数。
    • prompt_tokens: prompt 的 token 数。
    • completion_tokens: 返回的 token 数。
  • choices: 返回的结果,是一个数组。

    • finish_reason: 结束原因,有 stop,碰到停止词,length,达到最大 token 数,content_filter,被 censor 了,tool_calls,模型请求了工具。
    • index: 结果的索引。
    • logprobs: logprobs 字段。
    • message: 消息对象。

      • content: 消息内容。
      • role: 消息角色。
      • tool_calls: 工具调用。

        • id: 工具调用的唯一标识。
        • type: 工具类型。
        • function: 函数对象。

          • name: 函数名。
          • arguments: 函数参数。

Chat Completion Chunk Object

如果是流模式,会返回多个 chat completion chunk object,最后会返回一个 [DONE] 作为结束标志。

  • id: 请求的唯一标识。
  • created: 请求的时间,Unix 秒时间戳。
  • model: 模型名称。
  • service_tier: 服务等级。
  • system_fingerprint: 系统指纹,代表了调用时的系统环境。
  • object: 返回对象的名称,总是chat.chat.completion.chunk
  • usage: 同上。
  • choices: 同上。

Langchain 基础使用

Langchain 如今的基础是 LCEL,基本思想是管线化,即把每个组件变成一个管线,然后把管线串联起来。因为 python 没有自定义符号,所以使用|来表示管线,这也是 unix 管道的符号。

管线连接的方法是invoke,即上一个管线的invoke输出是下一个管线invoke的输入。

本文只需要一个简单的模型补全,因此我们的管线是 prompt_template | model | parser

Prompt Template

对话补全使用from langchain_core.prompts.ChatPromptTemplate

创建对话模版使用from_messages构造器,它接受一个元组列表,元组的第一个元素是消息的角色,第二个元素是消息的内容。

Langchain 使用简单的 f-string 模版,因此变量用{}包裹。

例如:

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "Hello, how can I help you?"),
    ("user", "{problem}")
])

input_str = prompt.invoke({"problem": "What's the weather like today?"})

这里我们使用这样一个模版:

prompt = ChatPromptTemplate.from_messages([
    ("system", '你是一个逻辑推理专家,擅长解决逻辑推理问题。以下是一个逻辑推理的题目,形式为单项选择题。所有的问题都是(close-world assumption)闭世界假设,即未观测事实都为假。请逐步分析问题并在最后一行输出答案,最后一行的格式为"答案是:A"。'),
    ("user", """### 题目:
{problem}

### 问题:
{question}
{options}""")
])

Model

Langchain 直接支持 Dashscope,因此我们可以直接使用 Dashscope 的模型。

from langchain_community.llms import Tongyi

model = Tongyi()

需要在环境变量中设置DASHSCOPE_API_KEY

Parser

直接使用一个StrOutputParser

from langchain_core.output_parsers import StrOutputParser

parser = StrOutputParser()

管线串联

chain = prompt | model | parser

然后这样调用即可:

def format_options(options):
    return '\n'.join(
        [
            f'{chr(ord("A") + i)}: {option}'
            for i, option in enumerate(options)
        ]
    )

res = chain.invoke({
    "problem": "有一个列表,找出该列表的最后一个元素。\n\n下列选项中哪个是列表 `[a, b, c, d]` 的最后一个元素?",
    "question": "选择题 1:",
    "options": format_options(["a", "b", "c", "d"])
})

这样就完成了一个简单的补全。

处理下数据即可复现baseline。