基于豆包的 LLM 开发入门实战

864 阅读10分钟

Demo

Demo 的功能很简单,通过与用户的语音对话,整理出今天花销的内容,然后展示在列表上。同时也支持通过对话对花销的内容进行删除或者修改

比如,用户说“今天晚上吃饭花了我56块钱”,列表就会展示出花销的内容是“晚餐”,金额是“56”,同时会自动识别出花销的类别是“餐饮”。

LLMExpenseTracker截图.png

实现思路

基本思路.png

实现的思路也很简单,通过将用户的语音转成文字,然后构建 Prompt 给到 LLM,让 LLM 提取出结构化的花销信息,该信息返回到 App 后,则通过列表展示出来。

那么接下来,我们一起看看如何基于豆包大模型把这个功能实现。

实战

1. 模型准备工作

首先我们选一个模型提供商,我们选择“火山引擎-豆包大模型”:www.volcengine.com/product/dou…

1️⃣ 选择模型

模型广场,选择一个模型,我们选择字节的 Doubao-pro 模型

2️⃣ 选择模型版本

点击“模型详情”后,我们还要选择对应的模型版本,这里我们选择带 Function call 的模型(为什么选择这个版本,我们后面会说到)

选择好对应的版本后,然后点击“推理”。

我们可以点击“体验”,进入的就是普通的大语言模型聊天窗口,在这里可以调试你的 Prompt

3️⃣ 创建在线推理接入点

在上一步点击推理后,我们就进入了创建在线推理的页面,填写接入点名称,选择购买方式是“按 Token 付费”(有50万免费的tokens额度),最后点击接入模型。

点击接入后,就会进入在线推理的界面,这里会列出你所有接入点,注意接入点名称下方会有一个 “模型名称” ,这个后续调用 API 时我们需要用到。

4️⃣ 创建 API Key

将鼠标挪到左边的导航栏,选择 API Key,我们需要再创建一个 “ API Key” ,就集齐所有需要的前置内容了。

API Key 是您请求火山方舟大模型服务的重要凭证。API Key 长期有效,请您不要将密钥信息共享至公开环境

2. 调通 API

我们使用 ChatCompletions-文字对话 的 API。

使用该接口可以向大模型发起文字对话请求。服务会将输入的文字信息输入给模型,并返回模型生成的内容。

baseURL: 
https://ark.cn-beijing.volces.com

path: 
/api/v3/chat/completions

header:
"Authorization": API_KEY

⚠️ API_KEY 请求头里添加的 API_KEY,就是上面我们创建的 API Key,注意这里需要加上 “Bearer ” 的前缀:

"Bearer 这里粘贴上面创建的APIKey"

🌰 请求示例:

可以看到请求参数里需要指定 model,model 就是我们上面创建推理接入点时的模型名称

curl https://ark.cn-beijing.volces.com/api/v3/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ea764f0f-3b60-45b3-****-************" \
  -d '{
    "model": "ep-20240704******-*****",
    "messages": [
        {
            "role": "system",
            "content": "You are a helpful assistant."
        },
        {
            "role": "user",
            "content": "Hello!"
        }
    ]
  }'

🌰 响应示例:

{
    "id": "021718067849899d92fcbe0865fdffdde********************",
    "object": "chat.completion",
    "created": 1720582714,
    "model": "doubao-pro-32k-240615",
    "choices": [{
        "index": 0,
        "message": {
            "role": "assistant",
            "content": "Hello, can i help you with something?"
        },
        "logprobs": null,
        "finish_reason": "stop"
    }],
    "usage": {
        "prompt_tokens": 22,
        "completion_tokens": 9,
        "total_tokens": 31
    }
}

选择你喜欢的网络请求库,用代码搭建起基本的请求逻辑吧。

3. 写 Prompt

前置准备工作就绪后,就可以开始调试我们的 Prompt 啦!

经过调试后,这是我的 Prompt:

你是一个专业的记帐智能助手,根据用户所说的话,选择一个记账动作来帮助用户,
请注意用下面动作对应的 JS0N 数据格式返回你的选择:
```
新增花销: {"name": "AddExpense", "parameters": {"id": String, "title": String, "amount": Double, "category": String}}
删除花销: {"name": "DeleteExpense", "parameters": {"id": String}
```

然后,我们在前面选择模型里,进入体验中心来测试我们的 Prompt:

看着很不错,已经按照我们的要求返回我们需要的内容了,并且对于类别的内容填充也很准确。

那么我们就吭哧吭哧的集成到代码里吧。

通过 API 发送的请求 JSON 是这样:

{
    "temperature": 0.8,
    "messages": [
        {
            "content": "你是一个专业的记帐智能助手,根据用户所说的话,选择一个记账动作来帮助用户,\n请注意用下面动作对应的 JS0N 数据格式返回你的选择:\n```\n新增花销: {"name": "AddExpense", "parameters": {"id": String, "title": String, "amount": Double, "category": String}}\n删除花销: {"name": "DeleteExpense", "parameters": {"id": String}\n```",
            "role": "system"
        },
        {
            "content": "今天买了一条裤子,花了我 10 块钱",
            "role": "user"
        }
    ],
    "tools": [],
    "model": "ep-20241108102003-xxx"
}

我们的基础 Prompt 就是填入 messages 里 role 为 system 的 content 里,用户输入的信息则是 role 为 user 的 conent 里。

返回的 JSON 数据是这样的:

{
    "choices": [
        {
            "finish_reason": "stop",
            "index": 0,
            "logprobs": null,
            "message": {
                "content": "{"name": "AddExpense", "parameters": {"id": "001", "title": "买裤子", "amount": 10, "category": "服装"}}\n",
                "role": "assistant"
            }
        }
    ],
    "created": 1731664097,
    "id": "0217316640965003d5a16fe195c821d80bfb0ac15cb4b72218a96",
    "model": "doubao-pro-32k-functioncall-240815",
    "object": "chat.completion",
    "usage": {
        "completion_tokens": 43,
        "prompt_tokens": 125,
        "total_tokens": 168
    }
}

解析 choices[0].message.content 里的内容为我们代码对应的 model 对象,便可以进行 AddExpense 的操作啦。

如果你实操执行到这里,可能已经发现出一些问题了:

  • 有时候模型返回给我们的,不是可以直接拿来用的 JSON;

  • 在添加花销时,模型给我们生成的 id 是从 1,2,3 这样递增,如果我们要对数据进行持久化,显然不是很好的选择,id 最好是由我们自己来生成,比如用 UUID;

  • 在执行删除操作时,模型给我们的 id 有时候也不一定准确

  • ……

总而言之,我们需要模型能够返回更准确的,我们所期盼的数据。

4. 使用 Function Calling

我们从生成 id 这一点切入,想一想如何才能让大模型返回一个我们所预期的 id 呢?既然它做不好这件事,那干脆就不让它生成了,我们需要一个机制让模型告诉我们去生成 id,然后再返回给它。

这个机制,就是 Function calling 功能。

Function calling 是一种将大模型与外部工具和API相连的新功能,助力大模型向实际产业落地迈进。

Function calling 允许开发者更可靠的从模型中获得结构化数据,无需用户输入复杂的 Prompt。

还记得前面我们选择模型的时候吗?我们特意选择了带 Function call 的豆包模型,该类模型对 Function calling 做了重点的调优,可以得到更好的效果。

豆包pro系列的函数调用模型。对任务解析与函数调用的能力进行了重点优化,在调用预定义函数、获取外部信息方面都有更好的效果,支持5万汉字左右的上下文窗口。

Function calling 的流程大概如下:

FunctionCalling流程图.png

回到我们这个 Demo 的情况,其实我们只需定义很少的 Prompt 就行,然后把记账的增删改查都定义成 Function,让模型识别出用户的意图,然后告诉我们该执行哪个 Function,App 就直接解析对应的 json 数据,然后本地代码去执行即可,因为这个 demo 的结果展示是通过 App 展示出来的,所以就没必要回传给模型让它给出最终的结果了。

为了方便理解,我们从对话的角度看“添加花销”和“删除花销” 就是像下面这样的:

Prompt.演示png.png

从上图可以看到,在添加花销时,模型直接返回对应的 JSON 数据,而我们收到该消息后,直接解析并且本地生成好 UUID 填充进去即可

然后在发起新一轮对话时,把已经生成好的花销填充到 Prompt 里

你是一个专业的记帐智能助手,根据用户所说的话,选择一个合适的记账 function 来执行。
当前记录的花销 :
[{ id :  "68D01731-D350-42FF-A931-8A8C34F1192C" , title :  "买裤子" , amount :  10.0 , category :  "服装" }] 

当用户说出“没有买裤子”时,模型就能识别需要调用删除的方法,并且从 Prompt 信息中关联上了对应的花销,就能直接组装出需要删除的 id 信息给到我们。

在新一轮对话里,把生成好的信息补充到 Prompt 里,这样做有一个好处是可以减少整体的上下文信息,避免浪费不必要的 Token

回到实际的 API 调用场景,按照豆包调用 Function calling 的方式,我们的请求数据如下:

{
    "model": "ep-20241108102003-xxx",
    "temperature": 0.8,
    "messages": [
        {
            "role": "system",
            "content": "你是一个专业的记帐智能助手,根据用户所说的话,选择一个合适的 function 来执行。\n当前记录的花销:\n[]"
        },
        {
            "role": "user",
            "content": "今天买了一条裤子,花了我 10 块钱"
        }
    ],
    "tools": [
        {
            "type": "function",
            "function": {
                "description": "添加一项花销",
                "parameters": {
                    "required": [],
                    "properties": {
                        "category": {
                            "type": "string",
                            "description": "花销的类别"
                        },
                        "title": {
                            "type": "string",
                            "description": "花销的内容"
                        },
                        "amount": {
                            "description": "花销的金额",
                            "type": "number"
                        }
                    },
                    "type": "object"
                },
                "name": "AddExpense"
            }
        },
        {
            "function": {
                "name": "DeleteExpense",
                "description": "删除一项花销",
                "parameters": {
                    "required": [
                        "id"
                    ],
                    "properties": {
                        "id": {
                            "description": "该花销的id",
                            "type": "string"
                        }
                    },
                    "type": "object"
                }
            },
            "type": "function"
        }
    ]
}

可以看到请求体里,多了 tools 这个节点,里面放置的就是告诉模型可以调用的方法,以及方法的参数定义。

一个 function 里包含三部分:

  • name:方法的名称。执行 Function calling 后返回的信息里会带上这个名称,注意方法名要简明扼要地表现出该方法做了什么,尽量不要有歧义。

  • description:方法描述。这里可以较为详细描述清楚方法做了什么,它其实也是 Prompt 的一部分,对于识别出用户的意图非常有用。

  • parameters:方法参数。通过定义好方法的参数,模型在识别出意图后,就会根据用户的信息填充到对应的参数里,最终给我们返回 JSON 格式的数据。(注意:模型还是有可能生成无效的 JSON 或虚构参数的,只是通过 function calling 的方式,这个概率大大减少)

模型在识别到用户的意图后,选择的一项最符合的 Function,填充好方法参数,最后返回的数据结构如下:

{
    "choices": [
        {
            "finish_reason": "tool_calls",
            "index": 0,
            "logprobs": null,
            "message": {
                "content": "\n",
                "role": "assistant",
                "tool_calls": [
                    {
                        "function": {
                            "arguments": "{"amount": 10, "category": "服装", "title": "买裤子"}",
                            "name": "AddExpense"
                        },
                        "id": "call_00hc07ktbewfpcs6t6qkhnit",
                        "type": "function"
                    }
                ]
            }
        }
    ],
    "created": 1731915961,
    "id": "021731915960598c67513de90d5a1c5468c332ec6425aef1b7ed2",
    "model": "doubao-pro-32k-functioncall-240815",
    "object": "chat.completion",
    "usage": {
        "completion_tokens": 37,
        "prompt_tokens": 291,
        "total_tokens": 328
    }
}

可以看到,返回的结构里,多了一个 tool_calls 的节点,里面的信息告诉我们需要调用的方法是 AddExpense,其参数是 {"amount": 10, "category": "服装", "title": "买裤子"}

基于这些信息,我们就可以很方便地在本地调用添加花销的方法了。

其余的删除,修改等功能,也是类似的实现,这里就不赘述了。

总结

本次实战我们基于记账这个很普通的功能,利用 LLM 的能力,做了新的交互实现。在实战过程中,我们了解到了基于 LLM 开发的一些基本 API 交互和套路。 在 LLM 时代下,我们可以对现有的很多事情,都重新思考一下有什么创新的解法,或许以前天马行空的想法,现在基于 LLM 也可以实现了。

本实战 Demo 代码:LLMExpenseTracker