大模型比赛2.3智能体开发(工具调用智能问答助手)

学校希望做一个问答助手,

学生用自然语言提问,助手能查课程安排、教师办公信息,并完成学时计算。请用大模型实现,使其能够自主调用下列三个工具:

  1. query_course_time(course_name):查课程上课时间、教室
  2. query_teacher_office(teacher_name):查老师办公室、答疑时间
  3. 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:

  1. JSON Schema:接口说明书(空壳,无数据、无代码)

  2. 解析函数 parse_tool_call:只负责解析模型输出的 JSON 字符串,提取 工具名、参数,它不带数据库、不带课程表

  3. 业务工具函数(query_course_time):这里才存放课程表 / 数据源,接收解析出来的参数,查询数据返回结果

  4. 用户完全不需要输入任何 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
   """
  1. 这一段是写代码的时候就预先定义好的,程序一跑,它就存在了
  2. 它被拼进 system 消息,自动送给 Qwen
  3. 用户从头到尾看不见、也不用管这个 JSOn

  1. ✅ JSON Schema 定义 3 个工具,工具清单写入对话模板(system 提示词)
  2. ✅ 编写解析函数:提取工具名 + 参数
  3. ✅ 多轮循环:执行工具→结果回灌模型→直到输出最终文字回答
  4. ✅ 输入两道测试问题,可直接运行截图
  1. JSON Schema:接口说明书(空壳,无数据、无代码)
  2. 解析函数 parse_tool_call:只负责解析模型输出的 JSON 字符串,提取 工具名、参数,它不带数据库、不带课程表
  3. 业务工具函数(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,不要多余文字;不需要工具直接回答中文。
"""

讲解

  1. tools 是我们写代码的时候提前写死 的,不是用户输入,用户只输入中文问句,不用写JSON。

  2. 这段JSON Schema只是接口契约(空壳说明书) :告诉模型工具名字、用途、需要什么参数。这里没有课程表、没有计算公式。

  3. 代码自动把 tool_json_str 拼到 system_prompt。

  4. 后续组装对话:

    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

讲解,结合运行过程

  1. 模型推理之后输出一段文本(内部消息,不给用户看,代码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":"数据结构"}}')
  1. 函数执行步骤:
  • strip():去掉换行空格;
  • json.loads():字符串转字典;
  • 提取 name 和 arguments;
  • 如果不是合法JSON(模型直接输出中文答案),触发异常,返回 None, None。
  1. 运行结果: 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一步步走代码流程:输入:《数据结构》什么时候上课?在哪上?

  1. messages 组装好 system+user,送入模型;
  2. 模型输出:{"name":"query_course_time","arguments":{"course_name":"数据结构"}};
  3. parse_tool_call 解析得到 tool_name="query_course_time", args={"course_name":"数据结构"};
  4. tool_map["query_course_time"] 拿到函数;执行 query_course_time(course_name="数据结构");
  5. 函数内部查 mock_db,返回 {"time": "周二第3-4节", "room": "教学楼306教室"};
  6. 把这个结果追加进 messages(回灌),再一轮循环,再次送入模型;
  7. 模型读到工具返回的数据,输出自然语言:《数据结构》在周二第3-4节上课,地点是教学楼306教室;
  8. 再次进入 parse_tool_call,这次模型输出是普通中文,解析得到 tool_name=None,return,给到用户。

拿题目2走流程:输入:《数据结构》每周4学时,一共16周,总学时多少?

  1. 模型输出JSON:{"name":"calc_credit_hours","arguments":{"per_week":4,"weeks":16}}
  2. 解析拿到工具名和参数;
  3. 执行 calc_credit_hours(per_week=4,weeks=16),得到 {"total_hours":64};
  4. 结果追加进messages回灌模型;
  5. 模型输出答案:该课程总学时为64学时。

易错点

❌ 误区:拿到tool_result直接return给用户 ✅ 纠正:原始结果交给模型,由模型组织成通顺中文。 ❌ 误区:回灌的时候重新发一遍tools清单 ✅ 纠正:回灌只追加工具查询出来的数据,system只在最开始一次加载。 ❌ 误区:tool_map是给模型读取的 ✅ 纠正:tool_map是Python调度用的字典,模型完全不知道它存在。

一句话串完整代码链路给学生

先写好tools(任务1,给模型看工具规范); 模型输出JSON指令交给 parse_tool_call 拆包(任务2); 用tool_map匹配业务函数,传入参数拿到数据,把数据追加回对话送给模型(任务3); 模型结合数据生成人话,返回给用户。

相关推荐
许彰午2 小时前
28-ORA-01110数据文件读取错误
数据库·oracle
凯凯姜2 小时前
MySQL 联合索引设计与索引失效
数据库·mysql
猫咪很可爱3 小时前
Oracle TEMP 表空间不足(ORA-01652)问题分析与优化实践
数据库·oracle
九零HTTP4 小时前
10月21号-CSDN配菜-IP池的5个坑
服务器·数据库·tcp/ip
Data-Miner4 小时前
需要做财务数据分析,有什么好用的工具?从准备到出报告的实操指南
大数据·数据库·数据分析
袋鼠云数栈5 小时前
让 SQL 解析不再重复造轮子:SqlParserX 正式开源
数据库·人工智能·sql·开源·数据治理
这个DBA有点耶5 小时前
期货 CTP 系统 Oracle RAC 迁移国产库实战:读写分离架构设计与性能对标
数据库·架构·dba
数据库安全5 小时前
AI数据访问安全 |如何控制AI访问权限,防止越权
数据库·安全
这个DBA有点耶6 小时前
Redo Log循环写与checkpoint推进:长事务阻塞下的Checkpoint风暴完整链路
数据库·mysql·代码规范