Demo
Demo 的功能很简单,通过与用户的语音对话,整理出今天花销的内容,然后展示在列表上。同时也支持通过对话对花销的内容进行删除或者修改。
比如,用户说“今天晚上吃饭花了我56块钱”,列表就会展示出花销的内容是“晚餐”,金额是“56”,同时会自动识别出花销的类别是“餐饮”。
实现思路
实现的思路也很简单,通过将用户的语音转成文字,然后构建 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 的流程大概如下:
回到我们这个 Demo 的情况,其实我们只需定义很少的 Prompt 就行,然后把记账的增删改查都定义成 Function,让模型识别出用户的意图,然后告诉我们该执行哪个 Function,App 就直接解析对应的 json 数据,然后本地代码去执行即可,因为这个 demo 的结果展示是通过 App 展示出来的,所以就没必要回传给模型让它给出最终的结果了。
为了方便理解,我们从对话的角度看“添加花销”和“删除花销” 就是像下面这样的:
从上图可以看到,在添加花销时,模型直接返回对应的 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