week2 day4 航空 AI 助手项目
一、项目总览
本项目基于 OpenAI 大模型 + Gradio 网页界面,从零搭建了航空公司 FlightAI 的智能客服助手,核心突破是引入了工具调用(Function Calling)能力,让 AI 不再只能基于训练知识回答,而是可以调用业务函数查询真实票价数据,并最终通过接入 SQLite 数据库完成工程化升级,实现了从 Demo 到可维护业务系统的演进。
技术栈:OpenAI API / Ollama 本地模型、Gradio、Python、SQLite、python-dotenv
二、项目迭代全流程
1. 基础版:纯对话式客服机器人
先搭建最小可用的对话框架,作为后续功能的载体:
- 系统提示词设定:明确 AI 角色为 FlightAI 客服,要求回答简短礼貌、不超过 1 句话、保证准确性,不知道答案直接说明,不编造信息。
- 核心回调函数:实现标准的
chat(message, history)逻辑 —— 转换 Gradio 历史消息格式 → 拼接系统提示 + 历史对话 + 用户提问 → 调用大模型 → 返回回答。 - 快速界面搭建:通过
gr.ChatInterface一行代码生成网页聊天界面,无需前端开发。
2. 进阶版:引入工具调用(Function Calling)
核心原理
大模型本身并不会执行代码,它的能力是识别用户意图后,输出「需要调用的函数名称 + 结构化参数」;由我们的业务代码接收参数、执行函数,再将执行结果以约定格式返回给大模型,最终由大模型把结果整理成自然语言回答用户。
实现步骤
-
定义业务工具函数先实现
get_ticket_price(destination_city)函数,初始版本用字典硬编码伦敦、巴黎、东京、柏林的票价,输入城市名即可返回对应票价。 -
编写工具描述(OpenAI 规范格式)按照大模型要求的结构,声明工具的名称、功能描述、参数类型、参数说明、必填项,包装成
tools列表传给大模型。这是模型能否正确调用工具的关键。 -
改造 chat 回调函数
- 调用大模型时传入
tools参数; - 判断响应的
finish_reason == "tool_calls":说明模型判定需要调用工具; - 解析模型返回的函数名和参数,执行本地工具函数;
- 将工具执行结果以
role: "tool"的格式追加到消息列表; - 再次调用大模型,让它基于工具返回的真实数据生成最终回答。
- 调用大模型时传入
迭代优化
- 从单次单工具调用升级为支持单次响应内多个工具并行调用,适配用户同时查询多个城市票价的场景;
- 从单次工具调用升级为
while循环多轮工具调用:覆盖模型需要连续多次调用工具才能完成回答的场景,保证逻辑闭环。
3. 工程化升级:接入 SQLite 数据库
改造内容
- 替换硬编码的票价字典,用 SQLite 数据库持久化存储「城市 - 票价」数据;
- 重写
get_ticket_price为数据库查询版本,新增set_ticket_price函数支持写入 / 更新票价; - 增加数据初始化脚本,批量写入基础票价数据,支持后续动态修改。
为什么要连接数据库?
这是从「演示 Demo」走向「可用业务系统」的关键一步,核心原因如下:
- 数据持久化:硬编码字典在程序重启后数据就会重置,数据库可以永久保存数据,票价修改后长期生效。
- 代码与数据解耦:票价属于业务数据,和代码逻辑分离,后续调整票价、新增城市不需要修改代码、重启服务,运维成本极低。
- 可维护性更强:支持标准的增删改查操作,后续批量更新价格、对接后台管理系统都更方便。
- 符合生产标准:真实业务场景中,票价、订单、用户信息等业务数据必然存储在数据库中,这是软件工程的标准实践。
- 拓展性更好:后续可以很方便地增加舱位、出行日期、折扣等数据维度,也可以关联订单、会员等其他业务表。
为什么选择 SQLite?
它是轻量嵌入式数据库,零配置、无需单独部署服务,单文件即可运行,非常适合小型项目、本地原型开发和演示场景。
三、核心最佳实践
1. 工具调用(Function Calling)最佳实践
- 描述精准无歧义:函数的功能说明、参数描述要清晰准确,直接决定大模型是否能正确触发工具、传入正确参数。
- 参数约束严格:明确标注必填参数、参数类型,设置
additionalProperties: false防止模型传入多余无效参数。 - 闭环处理多轮调用:使用
while循环持续判断finish_reason,覆盖模型连续多次调用工具的场景,避免回答中断。 - 返回格式标准化:严格按照规范返回包含
role="tool"、content、tool_call_id的结果,确保模型能正确识别并使用工具输出。 - 参数校验与异常兜底:执行函数前校验参数合法性,工具执行失败、查无数据时返回清晰的提示文本,让模型可以友好告知用户,避免程序崩溃。
- 添加调用日志:工具执行时打印日志(如城市名、调用结果),便于调试和排查问题。
2. 系统提示词最佳实践
- 明确角色与边界:清晰定义 AI 的身份、回答风格、长度限制(本例要求不超过 1 句话),保证输出统一。
- 设定准确性原则:明确要求 “不知道就直说,不编造信息”,尤其客服场景下错误信息会带来业务风险。
- 业务规则前置:将核心业务要求写入系统提示,从源头规范模型行为。
3. 工程化最佳实践
- 数据与代码分离:业务数据存入数据库,不硬编码在代码中,便于维护和迭代。
- 职责分层:数据库操作、工具函数、对话逻辑分层编写,代码结构清晰,后续新增工具更方便。
- 密钥安全管理:API 密钥通过
.env文件管理,不硬编码在代码里,避免泄露风险。 - 兼容多模型:代码预留了 Ollama 本地模型的切换入口,降低部署和成本调整的门槛。
4. 交互体验最佳实践
- 生产环境建议开启
stream=True流式输出,搭配yield实现打字机效果,大幅降低用户等待感。 - 保持回答风格与场景匹配:客服场景追求简短、专业、礼貌,避免冗余输出。
四、核心逻辑流程
plaintext
用户提问 → 组装「系统提示 + 历史对话 + 用户消息」 → 调用大模型(传入 tools) ↓ 若 finish_reason == tool_calls ↓ 解析函数名与参数 → 执行本地工具函数 → 工具结果追加到消息列表 → 再次调用大模型 ↓ 循环直到无工具调用 → 返回最终自然语言回答给用户
import os import json import sqlite3 from dotenv import load_dotenv from openai import OpenAI import gradio as gr # ===================== 1. 初始化配置 ===================== load_dotenv(override=True) openai_api_key = os.getenv('OPENAI_API_KEY') if openai_api_key: print(f"OpenAI API Key exists and begins {openai_api_key[:8]}") else: print("OpenAI API Key not set") MODEL = "gpt-4.1-mini" openai = OpenAI() # 可选:切换为本地 Ollama 模型 # MODEL = "llama3.2" # openai = OpenAI(base_url='http://localhost:11434/v1', api_key='ollama') # 系统提示词:航空客服角色设定 system_message = """ You are a helpful assistant for an Airline called FlightAI. Give short, courteous answers, no more than 1 sentence. Always be accurate. If you don't know the answer, say so. """ # ===================== 2. 数据库相关操作 ===================== DB = "prices.db" # 初始化数据库表 with sqlite3.connect(DB) as conn: cursor = conn.cursor() cursor.execute('CREATE TABLE IF NOT EXISTS prices (city TEXT PRIMARY KEY, price REAL)') conn.commit() def get_ticket_price(city): """工具函数:查询指定城市的机票价格""" print(f"DATABASE TOOL CALLED: Getting price for {city}", flush=True) with sqlite3.connect(DB) as conn: cursor = conn.cursor() cursor.execute('SELECT price FROM prices WHERE city = ?', (city.lower(),)) result = cursor.fetchone() if result: return f"Ticket price to {city} is ${result[0]}" else: return "No price data available for this city" def set_ticket_price(city, price): """写入/更新城市机票价格""" with sqlite3.connect(DB) as conn: cursor = conn.cursor() cursor.execute( 'INSERT INTO prices (city, price) VALUES (?, ?) ON CONFLICT(city) DO UPDATE SET price = ?', (city.lower(), price, price) ) conn.commit() # 初始化基础票价数据 initial_ticket_prices = {"london": 799, "paris": 899, "tokyo": 1420, "sydney": 2999} for city, price in initial_ticket_prices.items(): set_ticket_price(city, price) # ===================== 3. 工具定义(Function Calling 规范) ===================== price_function = { "name": "get_ticket_price", "description": "Get the price of a return ticket to the destination city.", "parameters": { "type": "object", "properties": { "destination_city": { "type": "string", "description": "The city that the customer wants to travel to", }, }, "required": ["destination_city"], "additionalProperties": False } } tools = [{"type": "function", "function": price_function}] # ===================== 4. 工具调用处理函数 ===================== def handle_tool_calls(message): """处理模型返回的单次/多次工具调用请求,返回工具执行结果列表""" responses = [] for tool_call in message.tool_calls: if tool_call.function.name == "get_ticket_price": arguments = json.loads(tool_call.function.arguments) city = arguments.get('destination_city') price_details = get_ticket_price(city) responses.append({ "role": "tool", "content": price_details, "tool_call_id": tool_call.id }) return responses # ===================== 5. 核心聊天回调函数 ===================== def chat(message, history): # 转换 Gradio 历史记录为 OpenAI 标准消息格式 history = [{"role": h["role"], "content": h["content"]} for h in history] # 组装消息列表 messages = [{"role": "system", "content": system_message}] + history + [{"role": "user", "content": message}] # 调用大模型,支持多轮工具调用循环 response = openai.chat.completions.create(model=MODEL, messages=messages, tools=tools) # 循环处理工具调用,直到模型不再需要调用工具 while response.choices[0].finish_reason == "tool_calls": message = response.choices[0].message tool_responses = handle_tool_calls(message) messages.append(message) messages.extend(tool_responses) # 再次调用模型,传入工具结果 response = openai.chat.completions.create(model=MODEL, messages=messages, tools=tools) return response.choices[0].message.content # ===================== 6. 启动 Gradio 界面 ===================== gr.ChatInterface(fn=chat, type="messages").launch()
功能验证测试
运行代码后,在弹出的网页中可以测试以下场景:
- 普通问候:
你好→ 模型直接礼貌回复,不调用工具 - 单城市票价:
去东京的机票多少钱?→ 模型自动调用查询工具,返回数据库中的价格 - 多城市票价:
伦敦和巴黎的票价分别是多少?→ 模型并行调用两次工具,统一整理回答 - 无数据城市:
去北京的机票多少钱?→ 工具返回无数据,模型如实告知用户
代码核心亮点
- 闭环工具调用:
while循环保证多轮工具调用都能被正确执行,逻辑无遗漏 - 数据持久化:票价存储在 SQLite 数据库中,重启服务数据不丢失
- 可扩展性强:新增工具只需定义函数 + 补充工具描述 + 在
handle_tool_calls中增加分支即可 - 兼容本地模型:预留 Ollama 切换入口,无需修改核心逻辑即可切换大模型

浙公网安备 33010602011771号