学校希望做一个问答助手,
学生用自然语言提问,助手能查课程安排、教师办公信息,并完成学时计算。请用大模型实现,使其能够自主调用下列三个工具:
query_course_time(course_name):查课程上课时间、教室query_teacher_office(teacher_name):查老师办公室、答疑时间calc_credit_hours(per_week, weeks):每周学时 × 周数,计算总学时 逻辑流程:用户自然语言提问 → 大模型判断要不要调用工具 → 输出工具调用 JSON → 代码解析、执行本地函数 → 把执行结果塞回给大模型 → 大模型整合输出自然语言答案,直到不需要工具,给出最终回答。

例如还是,Qwen(Qwen3.5-0.8B)文本大模型,不需要换别的模型
本地 Qwen调用,无 LangChain,纯原生,带注释、异常检查
用豆包完成一个ppt 功能的过程,对比比赛agent
| 对比项 | 比赛自主写的 Agent(Qwen) | 豆包官方对话 Agent |
|---|---|---|
| 工具 Schema 来源 | 自己手写 JSON Schema,一个工具对应数组内一条对象,所有工具合并在同一个 tools 数组,不分文件 | 平台后端提前内置写好PPT、搜索等工具的 JSON Schema,用户不用编写 |
| 交给模型的方式 | 1. 你在代码里写好全部工具的 JSON Schema 数组; 2. 调度器把【system 提示词 + tools 工具清单 + 历史对话 + 用户当前提问】打包成完整 message 上下文; 3. 上下文一次性发给 Qwen 大模型; 4. 模型读取工具描述,自动判断:要不要调用、调用哪一个、生成参数; 5. 调度器拿到 tool_call 指令,匹配本地同名函数,执行代码,拿到结果再塞回对话历史,继续多轮循环。 | ###### 核心区别:工具清单不是你手动写进 prompt,而是平台内置下发 1. 内置工具池:豆包服务端预先内置了 PPT 生成、代码解释器、联网搜索等一堆工具,每一个工具都提前写好了固定 JSON Schema,保存在服务端配置里,不需要你编写代码定义工具火山引擎。 2. 工具清单自动下发 你发起对话时,调度器会根据当前会话开启的插件,自动把对应工具的 JSON Schema 打包成 tools 列表,自动追加到发给模型的请求参数里,不需要你手动把工具文本写到对话里。 3. 模型拿到平台下发的工具清单,再结合你的问题,自主选择是否调用 PPT 工具; 4. 调度器捕获调用指令,调用平台内置的 PPT 服务,生成 pptx 文件,最后把下载链接返回给你。 |
| 每一轮请求特点 | 每一轮发给 LLM 的请求,都要带上这份 tools 清单;清单不存入 messages 对话历史 | 会话开启后,调度器自动携带已开启插件对应的工具清单,每轮请求自动附带 |
| LLM 判断依据 | 读取你手写的 description 描述,对比用户提问,决定是否调用、调用哪个工具 | 读取平台内置的工具描述,结合用户提问做语义匹配判断 |
| 修改工具 | 自己改 JSON、增删工具、修改描述,控制权在你代码里 | 用户不能修改内置工具的定义、描述;只能开启 / 关闭插件 |
| 对应流程 | 调度器打包【工具清单 + messages 对话】发给 Qwen → LLM 输出工具指令 → 调度器匹配本地 Python 函数执行 | 调度器自动带上内置工具清单发给大模型 → LLM 输出调用指令 → 调度器调用平台后端内置服务 |
说明1:
JSON Schema:接口说明书(空壳,无数据、无代码)
解析函数 parse_tool_call:只负责解析模型输出的 JSON 字符串,提取 工具名、参数,它不带数据库、不带课程表
业务工具函数(query_course_time):这里才存放课程表 / 数据源,接收解析出来的参数,查询数据返回结果
用户完全不需要输入任何 JSON!用户只输入自然中文问话。 工具列表(那一大段 JSON Schema),是我们程序员提前写死、在代码内部拼到 system 提示词里面的固定文本,不是用户输入的内容。
tools = [ { "type": "function", "function": { "name": "query_course_time", "description": "根据课程名称,查询上课时间与教室地点", "parameters": {...} } }, ... ] tool_json_str = json.dumps(tools, ensure_ascii=False, indent=2) system_prompt = f""" 你是校园问答助手,可用工具清单:{tool_json_str} 规则:xxx """
- 这一段是写代码的时候就预先定义好的,程序一跑,它就存在了
- 它被拼进 system 消息,自动送给 Qwen
- 用户从头到尾看不见、也不用管这个 JSOn


- ✅ JSON Schema 定义 3 个工具,工具清单写入对话模板(system 提示词)
- ✅ 编写解析函数:提取工具名 + 参数
- ✅ 多轮循环:执行工具→结果回灌模型→直到输出最终文字回答
- ✅ 输入两道测试问题,可直接运行截图

- JSON Schema:接口说明书(空壳,无数据、无代码)
- 解析函数 parse_tool_call:只负责解析模型输出的 JSON 字符串,提取
工具名、参数,它不带数据库、不带课程表 - 业务工具函数(
query_course_time):这里才存放课程表 / 数据源,接收解析出来的参数,查询数据返回结果
代码总逻辑
import json
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
# ===================== 任务1:JSON Schema 定义3个工具,通过对话模板(system提示词)传给模型 =====================
# 按照题目图片里的三个工具定义,严格对齐工具名、功能、参数
tools = [
{
"type": "function",
"function": {
"name": "query_course_time",
"description": "根据课程名称,查询上课时间与教室地点",
"parameters": {
"type": "object",
"properties": {
"course_name": {"type": "string", "description": "课程全名"}
},
"required": ["course_name"]
}
}
},
{
"type": "function",
"function": {
"name": "query_teacher_office",
"description": "根据教师姓名,查询办公地点与答疑时间",
"parameters": {
"type": "object",
"properties": {
"teacher_name": {"type": "string", "description": "教师姓名"}
},
"required": ["teacher_name"]
}
}
},
{
"type": "function",
"function": {
"name": "calc_credit_hours",
"description": "每周学时 × 上课周数,计算课程总学时",
"parameters": {
"type": "object",
"properties": {
"per_week": {"type": "integer", "description": "每周学时数"},
"weeks": {"type": "integer", "description": "总共上课周数"}
},
"required": ["per_week", "weeks"]
}
}
}
]
# 将工具列表转为格式化JSON字符串
tool_json_str = json.dumps(tools, ensure_ascii=False, indent=2)
# system提示词(对话模板):把工具清单交给大模型,规定输出规则
system_prompt = f"""
你是校园问答助手,只能调用下面JSON里定义的工具:
{tool_json_str}
执行规则:
1. 如果不需要调用工具,直接输出自然语言答案。
2. 如果需要调用工具,只输出纯JSON,格式严格为:{{"name":"工具名","arguments":{{参数键值对}}}}
3. 禁止额外文字、markdown标记、```符号。
4. 收到工具返回结果之后,整理成通顺中文给出最终回答,不再调用工具。
"""
# ===================== 加载本地 Qwen3.5-0.8B 分词器与模型 =====================
# 修改为你本机解压后的模型文件夹路径
model_path = "./Qwen3_5-0_8B"
try:
# 加载分词器:负责把文本转为模型认识的token序列
tokenizer = AutoTokenizer.from_pretrained(
model_path,
trust_remote_code=True
)
# 加载Qwen大模型,自动分配到可用GPU,半精度float16节省显存
model = AutoModelForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.float16,
device_map="auto",
trust_remote_code=True
)
except Exception as e:
# 捕获模型加载异常,方便排查路径错误、文件缺失
raise RuntimeError(f"模型加载失败,请检查路径:{e}")
# ===================== 任务2:解析函数,从模型输出中提取工具名和参数,带异常检查 =====================
def parse_tool_call(output_text: str):
"""
解析模型返回的文本,判断是否是工具调用指令
:param output_text: 模型推理后输出的字符串
:return: (tool_name, args_dict)
如果解析失败(不是工具调用JSON),返回 (None, None),代表模型要输出最终回答
"""
try:
# 去除首尾空白换行
text = output_text.strip()
# 把字符串转成字典
call_data = json.loads(text)
tool_name = call_data.get("name")
args = call_data.get("arguments", {})
# 简单校验字段类型,防止脏数据
if not isinstance(tool_name, str) or not isinstance(args, dict):
return None, None
return tool_name, args
except json.JSONDecodeError:
# JSON格式不对,说明模型输出普通中文答案,不是工具调用
return None, None
except Exception:
# 捕获其余未知异常
return None, None
# ===================== 三个工具的本地实现(模拟数据库,比赛时可以替换真实数据源) =====================
def query_course_time(course_name: str):
"""查询课程时间、教室"""
mock_db = {
"数据结构": {"time": "周二第3-4节", "room": "教学楼306教室"},
"人工智能基础": {"time": "周一1-2节", "room": "实训楼302"}
}
return mock_db.get(course_name, {"msg": "未找到该课程信息"})
def query_teacher_office(teacher_name: str):
"""查询老师办公室、答疑时间"""
mock_db = {
"王老师": {"office": "综合楼402", "time": "周三下午14:00-16:00"}
}
return mock_db.get(teacher_name, {"msg": "未查到该教师信息"})
def calc_credit_hours(per_week: int, weeks: int):
"""学时计算:每周学时 × 周数 = 总学时"""
total = per_week * weeks
return {"total_hours": total}
# 工具映射字典:工具名字符串 -> 对应python函数,调度器用来查找并执行函数
tool_map = {
"query_course_time": query_course_time,
"query_teacher_office": query_teacher_office,
"calc_credit_hours": calc_credit_hours
}
# ===================== 任务3:Agent主循环,自动调用工具、回灌结果、循环直到最终回答 =====================
def run_agent(user_query: str, max_loop=5):
"""
智能体主逻辑循环
:param user_query: 用户输入的自然语言问题
:param max_loop: 最大循环次数,防止无限死循环
:return: 模型给出的最终自然语言答案
"""
# 初始化对话历史,system放规则、user放提问
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_query}
]
# 循环,最多max_loop轮工具调用
for _ in range(max_loop):
# Qwen内置聊天模板,把messages拼接成模型接受的对话格式
chat_text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
# 将文本编码为张量,送到模型对应设备(GPU)
inputs = tokenizer([chat_text], return_tensors="pt").to(model.device)
# 推理阶段关闭梯度计算,节省显存、加速
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=256, # 单次最多生成token数量
do_sample=False, # 关闭随机采样,输出稳定可复现,方便截图提交
temperature=0.0 # 温度0,确定性输出
)
# 解码:只取出本次新生成的内容,去掉输入部分
response = tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:], skip_special_tokens=True)
print(f"\n【模型输出内容】:{response}")
# 调用解析函数,判断是不是工具调用
tool_name, args = parse_tool_call(response)
# tool_name为None,代表模型直接给出最终答案,结束循环返回结果
if tool_name is None:
return response
# 校验:模型输出的工具名不在我们定义的列表里,返回错误提示
if tool_name not in tool_map:
return f"错误:不存在该工具 {tool_name}"
try:
# 执行对应的工具函数
tool_result = tool_map[tool_name](**args)
except Exception as e:
return f"工具执行异常:{str(e)}"
print(f"【工具返回结果】:{json.dumps(tool_result, ensure_ascii=False)}")
# ========= 核心:工具结果回灌进对话历史,交给模型下一轮推理 =========
messages.append({"role": "assistant", "content": response})
messages.append({
"role": "user",
"content": f"工具返回结果:{json.dumps(tool_result, ensure_ascii=False)},请根据这个结果给出最终回答。"
})
# 循环次数耗尽,退出
return "达到最大循环次数,停止调用"
if __name__ == "__main__":
print("==== 本地Qwen工具调用助手,输入exit退出 ====")
while True:
# 任务4:交互式输入问题,输入指定两道题目运行截图
question = input("\n请输入你的问题:")
if question.strip().lower() == "exit":
break
final_ans = run_agent(question)
print(f"\n✅【最终回答】:{final_ans}")
实验分步讲解(结合代码逐流程,任务1、2、3)
环境:本地Qwen3.5-0.8B,transformers原生代码,无LangChain 整体流程:用户输入中文 → system携带工具清单送入模型 → 模型输出JSON调用指令 → parse_tool_call解析 → tool_map匹配执行业务函数 → 结果追加回messages回灌模型 → 模型输出最终答案
任务1:用 JSON Schema 定义三个工具,并通过对话模板把工具清单交给模型
对应代码片段
# 【任务1】JSON Schema定义工具(接口契约,只是说明书,没有数据、没有执行逻辑)
tools = [
{
"type": "function",
"function": {
"name": "query_course_time",
"description": "根据课程名称,查询上课时间与教室地点",
"parameters": {
"type": "object",
"properties": {
"course_name": {"type": "string", "description": "课程全名"}
},
"required": ["course_name"]
}
}
},
{
"type": "function",
"function": {
"name": "calc_credit_hours",
"description": "计算课程总学时",
"parameters": {
"type": "object",
"properties": {
"per_week": {"type": "integer", "description": "每周学时"},
"weeks": {"type": "integer", "description": "上课周数"}
},
"required": ["per_week", "weeks"]
}
}
}
]
# 转为字符串,拼进system提示词
tool_json_str = json.dumps(tools, ensure_ascii=False, indent=2)
system_prompt = f"""
你是校园问答助手,可用工具:
{tool_json_str}
规则:需要调用工具,只输出纯JSON,不要多余文字;不需要工具直接回答中文。
"""
讲解
-
tools是我们写代码的时候提前写死 的,不是用户输入,用户只输入中文问句,不用写JSON。 -
这段JSON Schema只是接口契约(空壳说明书) :告诉模型工具名字、用途、需要什么参数。这里没有课程表、没有计算公式。
-
代码自动把
tool_json_str拼到system_prompt。 -
后续组装对话:
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": "《数据结构》什么时候上课?在哪上?"}
]
system 里的工具清单 + 用户问题,一起喂给Qwen模型。 5. 模型读懂这份说明书,自己判断:这个问题需要调用 query_course_time,然后输出JSON调用指令。
易错点
❌ 误区:tools里面存课程数据 ✅ 纠正:数据在后面 def query_course_time() 里面;tools只是给模型看的规范。 ❌ 误区:用户要输入tools这个JSON ✅ 纠正:tools是代码内置,用户看不见。
任务2:编写解析函数,从模型的输出中解析出工具名与参数
对应代码片段
# 【任务2】解析函数:只解析文本,不查表、不计算
def parse_tool_call(output_text: str):
try:
text = output_text.strip()
call_data = json.loads(text)
tool_name = call_data.get("name")
args = call_data.get("arguments", {})
if not isinstance(tool_name, str) or not isinstance(args, dict):
return None, None
return tool_name, args
except json.JSONDecodeError:
return None, None
except Exception:
return None, None
讲解,结合运行过程
-
模型推理之后输出一段文本(内部消息,不给用户看,代码print只是调试):
{"name":"query_course_time","arguments":{"course_name":"数据结构"}}
这串字符串,作为参数 output_text 传给 parse_tool_call。
tool_name, args = parse_tool_call(output_text='{"name":"query_course_time","arguments":{"course_name":"数据结构"}}')
- 函数执行步骤:
strip():去掉换行空格;json.loads():字符串转字典;- 提取
name和arguments; - 如果不是合法JSON(模型直接输出中文答案),触发异常,返回
None, None。
- 运行结果:
tool_name = "query_course_time"args = {"course_name": "数据结构"}这个函数完全没有课程表,它只负责拆包,把模型输出的指令拆开。
易错点
❌ 误区:解析函数里面写查询课程的逻辑 ✅ 纠正:解析只管拆文本;查询交给单独的业务函数。 ❌ 误区:这个JSON要展示给普通用户 ✅ 纠正:属于内部通信指令,正式版本删掉print,用户看不到。
任务3:执行工具并把结果回灌给模型(主循环调度逻辑)
对应配套业务函数 + 映射表
# 业务函数1:存放模拟数据源,查询课程
def query_course_time(course_name: str):
mock_db = {
"数据结构": {"time": "周二第3-4节", "room": "教学楼306教室"}
}
return mock_db.get(course_name, {"msg": "未找到该课程信息"})
# 业务函数2:计算学时
def calc_credit_hours(per_week: int, weeks: int):
total = per_week * weeks
return {"total_hours": total}
# 映射表:字符串工具名 -> Python函数(程序自己用,模型不知道这个字典)
tool_map = {
"query_course_time": query_course_time,
"calc_credit_hours": calc_credit_hours
}
主循环关键代码片段
def run_agent(user_query: str, max_loop=5):
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_query}
]
for _ in range(max_loop):
# 把对话送入模型推理
chat_text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer([chat_text], return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model.generate(**inputs, max_new_tokens=256, temperature=0.0)
response = tokenizer.decode(outputs[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
print(f"【模型中间输出(调试)】:{response}")
# 任务2:调用解析函数
tool_name, args = parse_tool_call(response)
if tool_name is None:
# 不用调用工具,直接返回答案给用户
return response
# 根据解析出来的名字,找到业务函数,执行工具
func = tool_map[tool_name]
tool_result = func(**args)
print(f"【工具返回结果】:{tool_result}")
# ========== 回灌核心:把工具查到的数据追加进对话 ==========
messages.append({"role": "assistant", "content": response})
messages.append({"role": "user", "content": f"工具返回结果:{tool_result},请根据该结果给出中文回答。"})
return "超出最大调用次数"
拿题目1一步步走代码流程:输入:《数据结构》什么时候上课?在哪上?
messages组装好 system+user,送入模型;- 模型输出:
{"name":"query_course_time","arguments":{"course_name":"数据结构"}}; parse_tool_call解析得到tool_name="query_course_time", args={"course_name":"数据结构"};tool_map["query_course_time"]拿到函数;执行query_course_time(course_name="数据结构");- 函数内部查
mock_db,返回{"time": "周二第3-4节", "room": "教学楼306教室"}; - 把这个结果追加进
messages(回灌),再一轮循环,再次送入模型; - 模型读到工具返回的数据,输出自然语言:
《数据结构》在周二第3-4节上课,地点是教学楼306教室; - 再次进入
parse_tool_call,这次模型输出是普通中文,解析得到tool_name=None,return,给到用户。
拿题目2走流程:输入:《数据结构》每周4学时,一共16周,总学时多少?
- 模型输出JSON:
{"name":"calc_credit_hours","arguments":{"per_week":4,"weeks":16}} - 解析拿到工具名和参数;
- 执行
calc_credit_hours(per_week=4,weeks=16),得到{"total_hours":64}; - 结果追加进messages回灌模型;
- 模型输出答案:该课程总学时为64学时。
易错点
❌ 误区:拿到tool_result直接return给用户 ✅ 纠正:原始结果交给模型,由模型组织成通顺中文。 ❌ 误区:回灌的时候重新发一遍tools清单 ✅ 纠正:回灌只追加工具查询出来的数据,system只在最开始一次加载。 ❌ 误区:tool_map是给模型读取的 ✅ 纠正:tool_map是Python调度用的字典,模型完全不知道它存在。
一句话串完整代码链路给学生
先写好tools(任务1,给模型看工具规范); 模型输出JSON指令交给 parse_tool_call 拆包(任务2); 用tool_map匹配业务函数,传入参数拿到数据,把数据追加回对话送给模型(任务3); 模型结合数据生成人话,返回给用户。