文章目录
博客详细讲解视频
点击查看高清脑图
dify工作流应用添加了参数提取节点,为了方便大家测试,https://weather.hanfangyuan.cn/weather
接口可以直接让大家调用,文末提供了工作流的DSL定义 (2024.11.21)
我们以一个天气查询工具为例子,介绍如何在dify平台创建自定义工具
1. 搭建天气查询http服务
1.1. flask代码
我们使用flask搭建简单的http服务,代码如下
from flask import Flask, request, jsonify
import random
app = Flask(__name__)
@app.route('/weather', methods=['POST'])
def get_weather():
auth_header = request.headers.get('Authorization')
if auth_header != 'Bearer hanfangyuan':
return {"msg": "Invalid Authorization header"}, 403
city = request.json.get('city', None)
if city is None:
return jsonify({
'status': 'error',
'errorInfo': 'No city provided',
'data': None
})
# 随机生成温度,风速和风向
temperature = f'{random.randint(10, 20)}℃'
windspeed = f'{random.randint(1, 5)}级'
winddirect = random.choice(['北风', '南风', '西风', '东风']) # 随机选择风向
# 返回JSON格式的响应
# return jsonify({
# 'status': 'OK',
# 'errorInfo': None,
# 'data': {
# 'city': city,
# 'temp': temperature,
# 'windspeed': windspeed,
# 'winddirect': winddirect
# }
# })
# 返回对LLM友好的字符串格式的响应
return f"{city}今天是晴天,温度{temperature}, 风速{windspeed}, 风向{winddirect}"
if __name__ == '__main__':
app.run(host='0.0.0.0',debug=False, port=4397)
1.2. 接口优化方法
在编写工具的http服务时我们有2个优化方向,即LLM调用友好和LLM理解友好,我在之前写的文章【LLMOps】如何借助AI实现智能客服有过介绍,下面我重新写一下:
LLM调用友好:多步骤整合
我们知道,调用哪些工具,以及根据工具的返回结果回答客户问题完全是依靠模型实现的。工具越多,工具调用的步骤越复杂,工具返回的结果越复杂,模型可能会出错。为了降低这个错误率,对模型的能力要求就会更高,同时模型的使用成本也会更高。为了避免这种情况,我们可以把多步骤的接口合并成一个,让AI模型直接调用。比如查询天气可能需要调用3个接口:鉴权、订阅、查询天气,如果直接让AI使用这三个工具,AI需要三个步骤才能获取到订单数据,对模型的要求就会升高,问题回答速度也会变慢。我们就可以把这三个接口合并成一个接口,对外提供服务。大多数情况,为了不影响原来的业务,我们可能无法改动这三个接口,所以我们可以专门做一个接口整合的服务,去中转这些复杂的接口,只提供给模型易用的接口。
LLM理解友好:自然语言式的返回结果
另外一个优化的方向是工具返回的结果,还是以天气查询接口为例,如果接口直接返回一个json结构,而且字段中都是英文缩写,AI可能根本无法理解这些字段的含义。一种方法是我们可以在提示词中预先告诉模型每个字段的含义,但是这样不够方便。最直接的方法是我们把接口返回的信息翻译成模型容易理解的文字,这样模型更容易理解这个结果。
2. 生成openapi json schema
2.1. 测试接口
在运行上述代码,搭建好天气查询服务后,我们首先需要测试一下这个接口,保证能够调通这个接口,可以利用ApiFox进行测试。
我首先把代码部署到我的服务器上,地址为https://weather.hanfangyuan.cn/weather,然后测试接口。
首先填写请求方法和地址,然后不要忘记填写认证的Authorization header
然后填写body,发送请求,确认接口正确返回数据
2.2. 生成openapi schema
- 接口测试无误后,我们按照下图方式导出接口curl命令
- 复制curl请求代码
- 利用gpt4把curl命令转为openapi schema
提示词如下
请把curl请求命令转成openapi 3.1.0 版本的json schema,不需要包含response信息
<curl>
curl --location --request POST 'https://weather.hanfangyuan.cn/weather' \
--header 'Authorization: Bearer hanfangyuan' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--header 'Content-Type: application/json' \
--data-raw '{
"city": "上海"
}'
</curl>
json schema请参照下面的例子
<json-schema>
{
"openapi": "3.1.0",
"info": {
"title": "Get weather data",
"description": "Retrieves current weather data for a location.",
"version": "v1.0.0"
},
"servers": [
{
"url": "https://weather.example.com"
}
],
"paths": {
"/location": {
"get": {
"description": "Get temperature for a specific location",
"operationId": "GetCurrentWeather",
"parameters": [
{
"name": "location",
"in": "query",
"description": "The city and state to retrieve the weather for",
"required": true,
"schema": {
"type": "string"
}
}
],
"deprecated": false
}
}
},
"components": {
"schemas": {}
}
}
</json-schema>
生成的json结果如下
{
"openapi": "3.1.0",
"info": {
"title": "Weather Service API",
"description": "API for retrieving weather data for a specified city.",
"version": "1.0.0"
},
"servers": [
{
"url": "https://weather.hanfangyuan.cn"
}
],
"paths": {
"/weather": {
"post": {
"description": "Retrieve weather data for a specific city",
"operationId": "getWeatherData",
"requestBody": {
"description": "City for which to fetch weather",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "Name of the city"
}
},
"required": ["city"]
}
}
}
},
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"temperature": {
"type": "number",
"description": "Current temperature in Celsius"
},
"description": {
"type": "string",
"description": "Weather condition description"
}
}
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
}
}
},
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
}
}
这个open api schema包含了天气工具的作用、url地址、请求参数、参数的描述等信息,利用这些信息能够正确创建http请求,并且能够提供模型这个工具的作用,指导模型什么时候应该调用这个工具。
3. 在dify中创建自定义工具
3.1. 导入schema
- 进入dify工具页面
- 创建自定义工具
- 填写名称
- 填写生成的open api json schema
3.2. 设置工具认证信息
- 点击设置鉴权方法
- 鉴权类型 API Key
- 头部Bearer
- 键 Authorization
- 值 hanfangyuan
配置上面认证方法的依据是,我们在1.1 flask 的代码设置了鉴权,具体代码如下
def get_weather():
auth_header = request.headers.get('Authorization')
if auth_header != 'Bearer hanfangyuan':
return {"msg": "Invalid Authorization header"}, 403
3.3. 测试工具
- 点击测试
- 填写参数值上海
- 点击测试
- 确认正常返回
测试正常后不要忘了点击右下角保存
4. 调用工具
4.1. Agent应用中调用工具
4.1.1. 创建agent应用
在工作室页面按照如下步骤创建agent类型应用
4.1.2. 添加工具
创建成功后按照如下步骤添加工具
4.1.3. 测试工具
工具添加完毕后,发送问题今天上海天气如何,可以看到成功调用工具,并返回结果。
4.2. 聊天助手工作流中调用工具
4.2.1. 创建工作流应用
- 工作室页面
- 创建空白应用
- 选择聊天助手
- 选择工作流编排
- 输入名称,工具调用-工作流
创建聊天工作流,在聊天工作流中调用工具(2024.11.21更新)
附工作流DSL
app:
description: ''
icon: 🤖
icon_background: '#FFEAD5'
mode: advanced-chat
name: 天气工具测试
use_icon_as_answer_icon: false
kind: app
version: 0.1.3
workflow:
conversation_variables: []
environment_variables: []
features:
file_upload:
allowed_file_extensions:
- .JPG
- .JPEG
- .PNG
- .GIF
- .WEBP
- .SVG
allowed_file_types:
- image
allowed_file_upload_methods:
- local_file
- remote_url
enabled: false
fileUploadConfig:
audio_file_size_limit: 50
batch_count_limit: 5
file_size_limit: 15
image_file_size_limit: 10
video_file_size_limit: 100
workflow_file_upload_limit: 10
image:
enabled: false
number_limits: 3
transfer_methods:
- local_file
- remote_url
number_limits: 3
opening_statement: ''
retriever_resource:
enabled: true
sensitive_word_avoidance:
enabled: false
speech_to_text:
enabled: false
suggested_questions: []
suggested_questions_after_answer:
enabled: false
text_to_speech:
enabled: false
language: ''
voice: ''
graph:
edges:
- data:
isInIteration: false
sourceType: parameter-extractor
targetType: tool
id: 1732149963373-source-1732150003764-target
source: '1732149963373'
sourceHandle: source
target: '1732150003764'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: tool
targetType: llm
id: 1732150003764-source-1732150044010-target
source: '1732150003764'
sourceHandle: source
target: '1732150044010'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: llm
targetType: answer
id: 1732150044010-source-answer-target
source: '1732150044010'
sourceHandle: source
target: answer
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: start
targetType: question-classifier
id: 1732149947407-source-1732150093814-target
source: '1732149947407'
sourceHandle: source
target: '1732150093814'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: question-classifier
targetType: parameter-extractor
id: 1732150093814-1-1732149963373-target
source: '1732150093814'
sourceHandle: '1'
target: '1732149963373'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: question-classifier
targetType: llm
id: 1732150093814-2-1732150120657-target
source: '1732150093814'
sourceHandle: '2'
target: '1732150120657'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: llm
targetType: answer
id: 1732150120657-source-1732150201925-target
source: '1732150120657'
sourceHandle: source
target: '1732150201925'
targetHandle: target
type: custom
zIndex: 0
nodes:
- data:
desc: ''
selected: false
title: 开始
type: start
variables: []
height: 53
id: '1732149947407'
position:
x: 18
y: 296
positionAbsolute:
x: 18
y: 296
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 243
- data:
answer: '{{#1732150044010.text#}}'
desc: ''
selected: false
title: 直接回复
type: answer
variables: []
height: 102
id: answer
position:
x: 1580
y: 282
positionAbsolute:
x: 1580
y: 282
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 243
- data:
desc: ''
model:
completion_params:
temperature: 0.7
mode: chat
name: gpt-4o-mini
provider: openai
parameters:
- description: City name
name: city
options: []
required: true
type: string
query:
- sys
- query
reasoning_mode: prompt
selected: false
title: 参数提取器
type: parameter-extractor
variables: []
vision:
enabled: false
height: 97
id: '1732149963373'
position:
x: 680
y: 282
positionAbsolute:
x: 680
y: 282
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 243
- data:
desc: ''
provider_id: 79c533bc-0e79-4a4d-88b8-5d8c1a9de752
provider_name: 天气
provider_type: api
selected: false
title: getWeather
tool_configurations: {}
tool_label: getWeather
tool_name: getWeather
tool_parameters:
city:
type: mixed
value: '{{#1732149963373.city#}}'
type: tool
height: 53
id: '1732150003764'
position:
x: 980
y: 282
positionAbsolute:
x: 980
y: 282
selected: true
sourcePosition: right
targetPosition: left
type: custom
width: 243
- data:
context:
enabled: false
variable_selector: []
desc: ''
model:
completion_params:
temperature: 0.7
mode: chat
name: gpt-4o-mini
provider: openai
prompt_template:
- id: ba104ff7-f4a0-4d29-9ad1-00bdb4997685
role: system
text: '# 角色
你是天气助手,你可以回答用户天气信息
# 天气信息
{{#1732150003764.text#}}
# 问题
{{#sys.query#}}'
selected: false
title: LLM
type: llm
variables: []
vision:
enabled: false
height: 97
id: '1732150044010'
position:
x: 1288
y: 287
positionAbsolute:
x: 1288
y: 287
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 243
- data:
classes:
- id: '1'
name: 查询天气
- id: '2'
name: 其他问题
desc: ''
instructions: ''
model:
completion_params:
temperature: 0.7
mode: chat
name: gpt-4o-mini
provider: openai
query_variable_selector:
- '1732149947407'
- sys.query
selected: false
title: 问题分类器
topics: []
type: question-classifier
vision:
enabled: false
height: 175
id: '1732150093814'
position:
x: 380
y: 282
positionAbsolute:
x: 380
y: 282
sourcePosition: right
targetPosition: left
type: custom
width: 243
- data:
context:
enabled: false
variable_selector: []
desc: ''
model:
completion_params:
temperature: 0.7
mode: chat
name: gpt-4o-mini
provider: openai
prompt_template:
- id: 4e8d3ec9-cac1-4158-be19-e611ad259331
role: system
text: 你是有用的助手
- id: 4ae058f4-a38b-43aa-a0f7-daa57be34984
role: user
text: '{{#sys.query#}}'
selected: false
title: LLM 2
type: llm
variables: []
vision:
enabled: false
height: 97
id: '1732150120657'
position:
x: 680
y: 418
positionAbsolute:
x: 680
y: 418
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 243
- data:
answer: '{{#1732150120657.text#}}'
desc: ''
selected: false
title: 直接回复 2
type: answer
variables: []
height: 102
id: '1732150201925'
position:
x: 983
y: 418
positionAbsolute:
x: 983
y: 418
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 243
viewport:
x: -308.5
y: 35
zoom: 1