勇哥注:
这一节使用一个天气查询的例子来测试LLM调用的function功能。
这个功能是这样的:
用户:”告诉我北京的天气“
LLM无法直接回答这个问题,于是它把这个问题进行分析,解构出一些参数,用这些参数去调用用户定义好的一个函数。
最后用用户自定义函数返回的结果,通过自然语言表述给用户。
LLM:
-----------------
北京的天气情况是:
- 温度:25摄氏度
- 预报:天气晴朗,微风
-----------------
这个过程中,会两次访问LLM,第一次是用户的问题”告诉我北京的天气“,第二次是追加上用户函数的返回结果,
然后第二次LLM就根据用户函数返回结果,组织出自然语言给用户。
注意:
模型是qwen-max,不要改为其它的模型。
模型一变,其返回的数据格式会变化,影响到message.function_call这种语句,会出运行问题。
因数数据格式一变,对象的访问格式就要跟着变。
即使大家都是使用dashscope风格也会有这种问题。
源码:
import json
import os
import dashscope
api_key=os.environ.get("DASHSCOPE_API_KEY")
dashscope.api_key=api_key
print(api_key)
#编写自己的天气函数
#先用每个城市的固定气温进行返回。
def get_weather(city,unit="摄氏度"):
temperature=-1
if city=="北京":
temperature=25
elif city=="上海":
temperature=28
elif city=="广州":
temperature=24
elif city=="深圳":
temperature=26
wether_info={
"city":city,
"temperature":temperature,
"unit":unit,
"forecast":["天气晴朗","微风"]
}
return json.dumps(wether_info,ensure_ascii=False)
#封装模型响应函数
def call_model(messages):
try:
completion = dashscope.Generation.call(
model="qwen-max", #"deepseek-v3.2-exp",
messages=messages,
result_format="message",
functions=functions,
stream=False, # 为了简单起见,先关闭流式输出
)
return completion
except Exception as e:
print(f"调用模型时出错: {e}")
return None
# 使用function call进行QA
def qa_weather(query):
messages=[
{"role":"user","content":query}
]
# 得到第一次响应
response=call_model(messages)
if not response or not response.output:
return "模型响应为空"
print(response)
message=response.output.choices[0].message
messages.append(message)
print('message=',message)
tool_call = message.function_call
if not tool_call:
return "模型未返回函数调用"
# 获取函数名和参数(从function字段访问)
function_name = tool_call["name"]
arguments_str = tool_call["arguments"]
print(f"函数名: {function_name}, 参数字符串: {arguments_str}")
try:
# 解析参数 - 处理空参数字符串的情况
city = None
unit = "摄氏度"
if not arguments_str.strip():
print(f"参数字符串为空: '{arguments_str}'")
# 尝试从用户原始查询中提取城市信息
# 简单提取常见城市名称
cities = ["北京", "上海", "广州", "深圳"]
for c in cities:
if c in query:
city = c
print(f"从查询中提取到城市: {city}")
break
if not city:
print("未能从查询中提取城市信息")
else:
# 正常解析JSON参数
arguments = json.loads(arguments_str)
city = arguments.get("city")
unit = arguments.get("unit", "摄氏度")
print(f"解析到城市: {city}, 单位: {unit}")
if city:
# 获取天气信息
weather_info = get_weather(city, unit)
print(f"天气信息: {weather_info}")
# 添加函数响应
messages.append({
"role": "function",
"name": function_name,
"content": weather_info
})
# 第二次请求
second_completion = call_model(messages)
if not second_completion or not second_completion.output:
return "第二次模型响应为空"
# 获取第二次响应的内容
if second_completion:
output = second_completion.output
if output.choices:
return output.choices[0].message.content
else:
return "请明确指定要查询天气的城市"
except (json.JSONDecodeError, Exception) as e:
print(f"处理函数调用时出错: {str(e)}")
import traceback
traceback.print_exc()
return "我无法回答您的问题"
functions=[
{
"name":"get_weather",
"description":"获取指定城市的天气信息",
"parameters":{
"type":"object",
"properties":{
"city":{
"type":"string",
"description":"城市名称"
},
"unit":{
"type":"string",
"description":"温度单位,默认摄氏度"
}
},
"required":["city"]
}
}
]
if __name__=="__main__":
query="北京的天气"
answer=qa_weather(query)
print(answer)代码的一些分析:
(一)第一次call_model(messages)
这里LLM收到用户的查询 ”北京的天气“
然后LLM返回的内容最重要的是不是用户自定义函数get_weather的信息?
而这些信息来源于字典 functions ?
回答:
--------------------------------
是的,第一次调用call_model(messages)时,LLM收到用户查询"北京的天气"后,
返回的内容中最重要的确实是与用户自定义函数get_weather相关的信息,
而这些信息正是来源于代码中定义的functions字典。
详细来说,执行过程如下:
1. 当调用call_model(messages)时,系统会将messages(包含用户查询"北京的天气")
和functions字典一起发送给LLM模型(qwen-max)。
2. functions字典定义了可供LLM使用的工具(函数)信息,包括函数名称、描述和参数结构。
在这个例子中,它定义了get_weather函数及其所需参数。
3. LLM根据用户的查询内容和functions中提供的函数信息,
判断需要调用get_weather函数来获取北京的天气信息。
4. 因此,LLM返回的响应中包含了一个function_call对象,
指明了要调用的函数名称(get_weather)和相关参数信息。
5. 代码随后从这个function_call对象中提取函数名和参数,并执行相应的函数调用来获取天气数据。
所以,functions字典实际上是在告诉LLM:"你可以使用这些工具来完成任务",
而LLM会根据用户查询决定是否使用以及如何使用这些工具。
(二)问题一的追加提问
(1)functions字典的key,如”name“,"description","parameters","required"
是不是dashscope约定好的?
(2)如果用户查询"你是谁",而不是”北京的天气“,是不是LLM就会判定这个问题跟查询天气无关,
就主动不会调用get_weather了?
(3)如果用户查询"西京的天气"时,LLM会纠正错别字为”北京“,让它做为city参数吗?
回答:
--------------------------
(1)是的,functions字典中的键名如"name"、"description"、"parameters"、"required"
等是dashscope API约定的标准格式。
这些字段符合OpenAI的Function Call规范,大多数支持函数调用的LLM API都采用类似的结构。
这种标准化的结构让LLM能够理解每个函数的名称、用途、所需参数及其类型要求。
(2)没错。当用户查询"你是谁"而不是"北京的天气"时,LLM会根据查询内容与functions中提供的函数描述进行匹配。
由于"你是谁"这个问题明显与天气查询无关,LLM会判定不需要调用get_weather函数,
而是直接以自然语言的形式回答用户的问题。
只有当用户查询与天气相关且需要获取实时或特定的天气信息时,LLM才会选择调用对应的工具函数。
(3)当用户查询"西京的天气"时,LLM是否会纠正错别字为"北京"作为city参数,主要取决于两个因素:
1. LLM自身的理解能力 :在当前代码中,第一次调用 call_model 函数时,
会将用户查询和 functions 字典一起发送给 qwen-max 模型。
如果模型具备较强的意图理解和错别字纠正能力,它可能会识别出"西京"是"北京"的错别字,
并在返回的 function_call 中直接将 city 参数设置为"北京"。
2. 参数处理逻辑 :代码中存在空参数字符串的处理机制,
但从用户查询中提取城市信息的逻辑只检查精确匹配的常见城市名称(北京、上海、广州、深圳)。
如果LLM返回空参数或使用"西京"作为参数,当前代码无法自动将"西京"纠正为"北京",
因为它没有专门的错别字映射或模糊匹配机制。
(三)通过get_weather这个自定义函数取得天气信息weather_info后,
messages又把这个获取的天气信息追加进来了,准备二次调用call_model进行请求。
这里我要提问:
messages.append追加的字典中,"role","content","name" 这三个key,是不是也是dashscope的固定约定?
回答:
--------------------------
是的, messages.append 追加的字典中,"role"、"content"和"name"这三个key
是DashScope平台进行函数调用时的固定约定格式。
这种格式实际上是遵循了OpenAI Function Call的规范,而DashScope也采用了类似的接口设计。
具体来说:
- "role" : 必须设置为"function",表示这是一个函数调用的结果。
- "name" : 必须与之前调用的函数名称保持一致,在这里就是"get_weather",用于标识是哪个函数的返回结果。
- "content" : 包含函数执行后的返回结果,这里是天气信息的JSON字符串。
这种结构的作用是告诉模型:
1. 系统已经执行了指定名称的函数
2. 函数返回了特定的结果内容
当模型收到这种格式的消息后,它就能基于这些函数执行结果生成自然、连贯的回答给用户。
这种约定确保了多轮对话和工具调用的正确流程,是实现函数调用功能的标准做法。
(四)(1)在get_weather这个用户自定义函数中,
下面这个字典的内容,是不是用户完全自定义的,DashScope对此不会有任何约定?
wether_info={
"city":city,
"temperature":temperature,
"unit":unit,
"forecast":["天气晴朗","微风"]
}
(2)那就是,对于wether_info这个数据结构的key和值,LLM会把它们转为自然语言给用户了?
(3)key是不是一定要起名为能理解的单词? 它能是中文吗?
如果"city"这个key,我写成"aabb1"
"temperature",我写成"tt1"
这样LLM就无法理解了?
回答:
--------------------------
(1)是的, wether_info 字典的内容完全是用户自定义的,DashScope对此没有特定的约定要求。
这个字典是用户在自定义的 get_weather 函数中创建的,用于组织和返回天气相关的数据。
在函数调用(Function Call)机制中:
1. DashScope只关心函数的输入参数是否符合 functions 中定义的规范
2. 对于函数返回的数据结构,DashScope不做强制要求
3. 用户可以根据自己的需求自由定义返回的JSON结构
这里的 wether_info 字典包含了城市名称、温度、温度单位和天气预报等信息,
这种结构设计是为了方便后续处理和向用户展示天气数据。
当这个字典被JSON序列化并返回后,模型会使用这些信息来生成自然语言回答,
但DashScope本身不会限制或要求特定的数据字段。
需要注意的是,虽然结构可以自定义,但数据的完整性和准确性会影响模型最终生成的回答质量。
(2)是的,对于 wether_info 数据结构中的键和值,LLM会将它们转换为自然语言返回给用户。具体流程如下:
1. get_weather 函数返回包含结构化数据的JSON字符串,包含city(城市)、temperature(温度)、
unit(单位)、forecast(预报)等信息
2. 这些结构化数据通过 messages.append 以特定格式添加到对话历史中:
messages.append({
"role": "function",
"name": function_name, # 这里是"get_weather"
"content": weather_info # 这里是JSON字符串形式的天气信息
})3. 当第二次调用 call_model 时,LLM会接收整个对话历史,
包括用户的原始查询、函数调用信息和函数返回的结构化数据
4. LLM能够理解这些结构化数据的含义,并将其转化为自然、友好的语言描述
例如,如果 wether_info 包含北京的天气数据,模型可能会生成类似:
"北京当前温度为25摄氏度,天气晴朗,微风"这样的自然语言回答。
LLM会根据数据内容和用户的查询,生成合适的总结性回答,而不会直接输出原始的JSON格式数据。
(3)关于JSON数据结构中键名的命名,有以下几点需要说明:
1. 键名不一定必须是有意义的单词 :JSON格式本身允许使用各种字符串作为键名,包括无意义的字符组合。
2. 键名可以使用中文 :JSON完全支持中文键名,例如可以使用"城市"代替"city","温度"代替"temperature"。
3. 对LLM理解的影响 :
- 如果使用无意义的键名(如"aabb1"、"tt1"),LLM将难以理解这些字段的具体含义,
因为它无法从键名本身推断出数据的语义。
- 即使键名无意义,LLM可能会尝试根据上下文(如函数名、用户查询和数据值)
来推测数据的含义,但这种推测可能不准确。
- 使用有意义的键名(无论是英文还是中文)会大大提高LLM对数据的理解能力,
使生成的回答更加准确和自然。
4. 最佳实践 :为了获得最佳效果,建议使用描述性强、语义清晰的键名,
这样可以帮助LLM更好地理解数据结构并生成准确的自然语言回答。
总结来说,虽然技术上键名可以是任意字符串,但为了让LLM能正确理解并生成高质量的回答,
应该使用有意义的键名,无论它们是英文还是中文。
LLM系列文章:
---------------------------------------------------
deepseek的dashscope调用示例
http://www.skcircle.com/?id=2745
天气查询小例子:测试LLM调用的function功能
http://www.skcircle.com/?id=2746
千问多模态模型qwen-vl-plus:图片中的表格内容提取
http://www.skcircle.com/?id=2747


少有人走的路



















