之前和几位做 AI 应用的同行聊天,大家都提到一个现象:很多团队在模型效果上差距并不大,真正拉开差距的往往是工程化能力。尤其当 AI 进入业务落地阶段,如何设计 Agent、如何编排提示词、如何低成本部署模型、如何应对线上各种异常,成为决定项目成败的关键。这也让我想到那些在 AI 赛道上持续发力的北大校友团队,他们的技术路径各有侧重,但底层都有一个共同点:把算法、数据、工程三件事当作一个整体来推进。本文就从 AI 工程实践的角度,完整拆解一个 AI 应用从 0 到 1 的搭建过程,包括环境准备、Agent 核心逻辑、模型部署和线上排错,适合刚接触 AI 开发、或者已经在做原型但想走向工程的读者。
1. 背景与核心概念
1.1 什么是 AI 应用开发
AI 应用开发指的是在预训练大模型的基础上,构建能够解决实际业务问题的软件系统。与传统的规则引擎或机器学习小模型不同,今天的 AI 应用通常以 LLM(大语言模型)作为核心“大脑”,通过提示词、检索增强(RAG)、工具调用(Function Calling)、多轮对话等机制,让模型完成推理、生成、分析等任务。
这里需要区分两个容易混淆的概念:
- 模型训练:指用大规模数据调整模型参数,需要 GPU 集群、数据管道和算法团队。
- 模型应用:指使用已经训练好的模型,通过 API 或本地部署,构建上层业务逻辑。
大部分工程开发者真正需要掌握的是后者。北大校友在 AI 竞赛中的优势也恰恰体现在这里,他们往往既有算法基础,又愿意深入到工程细节中,而工程能力正是从 Demo 到产品之间最难跨越的鸿沟。
1.2 AI Agent 是什么
AI Agent(智能体)可以理解为一个“会使用工具的助手”。它不只是回答用户问题,还能根据问题自动决定调用哪些工具、如何拆分任务、如何校验结果。一个典型的 Agent 系统包含以下部分:
- 大模型:负责理解意图和生成推理步骤。
- 提示词模板:定义 Agent 的角色、能力和限制。
- 工具集:例如搜索、计算、数据库查询、HTTP 请求等。
- 执行循环:模型输出动作指令,系统执行并返回结果,模型再继续推理。
常见应用场景包括:智能客服、数据分析助手、自动报告生成、个性化推荐解释、企业知识库问答等。在实际项目中,Agent 的可靠性往往取决于工具设计的清晰度和失败处理策略,而不是模型本身。
1.3 为什么需要关注工程实践
很多初学者把大模型 API 调用当成 AI 应用的全部,但真实业务中还要考虑响应延迟、成本、上下文长度、错误恢复、安全合规、并发控制等问题。这也是为什么“AI 工程实践”和“AI 模型部署”逐渐成为团队能力建设的重点。本文后面会围绕一个具体的客服问答 Agent 项目,展示这些工程问题的解决思路。
2. 环境准备与版本说明
2.1 基础环境
本文示例以 Python 3.10 以上版本为参考,操作系统推荐 Linux 或 macOS,Windows 也可以运行,但部分命令行操作略有差异。你需要提前安装:
- Python 3.10+
- pip
- virtualenv 或 conda
- Git
版本需要根据你的项目实际情况调整。如果你使用的是较新的 Python 3.12,需要注意部分依赖库的兼容性。建议先创建独立虚拟环境,避免污染系统 Python。
# 创建一个 Python 3.10 虚拟环境 conda create -n ai-agent python=3.10 -y conda activate ai-agent在开始写代码之前,先确定你的大模型访问方式。本文示例使用 OpenAI 兼容接口,你可以替换成任意支持 compatible 模式的国内模型或本地部署服务。如果你使用本地模型,需要额外准备对应推理框架和显卡资源。
2.2 项目依赖
我们需要安装以下核心依赖:
openai:用于调用大模型接口,兼容 OpenAI 格式。fastapi和uvicorn:用于构建并启动 Web 服务。requests:用于 Agent 调用外部工具。python-dotenv:管理环境变量。
pip install openai fastapi uvicorn requests python-dotenv如果你后续要接入 LangChain 或 LlamaIndex,可以再按需安装。但本文刻意避免过度依赖框架,因为只有理解了底层逻辑,使用框架时才不会迷失在抽象概念里。
2.3 项目结构
我们用一个简单的项目结构来组织代码:
ai-agent-demo/ ├── agent.py # Agent 核心逻辑 ├── tools.py # 工具函数集合 ├── app.py # FastAPI Web 服务 ├── .env # 环境变量(不入库) └── requirements.txt # 依赖清单这样的结构适合小型项目。当业务复杂后,你可以按功能模块拆分成更细的包。
3. 核心语法、配置与原理拆解
3.1 大模型调用基础
在 Agent 系统中,大模型扮演“决策者”的角色。它接收用户请求,生成两种结果:一种是直接回答,另一种是“调用工具的指令”。为了让模型输出可解析的指令,通常会使用 Function Calling 能力。
以 OpenAI 兼容接口为例,一次最简单的调用如下:
from openai import OpenAI client = OpenAI( api_key="your-api-key", base_url="https://api.example.com/v1" ) resp = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "你是一个 AI 助手。"}, {"role": "user", "content": "你好"} ] ) print(resp.choices[0].message.content)这里的base_url可以替换为任何兼容 OpenAI 协议的服务。关键参数:
model:模型名称,不同服务商略有差异。messages:对话消息列表,包含 system、user、assistant 三种角色。temperature:控制随机性,0 到 2 之间。用于工具调用时建议调低,比如 0.1,避免指令不稳定。
3.2 Function Calling 原理
Function Calling 的本质是:你在请求中声明一组函数,模型根据用户问题决定是否需要调用函数,并返回结构化的 JSON 参数。系统再使用这个 JSON 参数实际执行函数,把结果回传给模型生成最终答案。
一个最小声明如下:
tools = [ { "type": "function", "function": { "name": "get_datetime", "description": "获取当前日期时间", "parameters": { "type": "object", "properties": {}, "required": [] } } } ]当用户问“现在几点”,模型会返回一个 tool_calls 结果,而不是直接回答。开发者的任务就是解析这个结果、执行函数、再次请求模型。
3.3 提示词设计的基本思路
提示词是 Agent 的行为说明书。好的提示词需要做到三点:
- 明确角色和目标。
- 给出限制条件。
- 给出处理失败时的兜底策略。
例如:
你是企业客服助手。你可以使用以下工具查询订单、查找物流和提供退换货说明。 如果用户的问题与业务无关,请礼貌拒绝回答。 如果工具返回结果为空,请如实告知用户“暂时无法查询”,不要编造信息。“不要编造信息”这句话很关键,因为大模型有时会产生“幻觉”,也就是生成看似合理但实际错误的答案。通过提示词约束可以降低幻觉概率,但不能完全消除,因此后续需要用工具结果约束模型输出。
3.4 工具函数的设计原则
工具函数是 Agent 和外部世界的桥梁。工具设计得越清晰,模型就越容易正确调用。推荐每个工具只做一件事,并给出详细的 description。比如:
query_order_status(order_id):根据订单号查询订单状态。calculate_delivery_days(city, date):计算预计送达天数。send_refund_request(order_id, reason):提交退换货申请。
工具参数命名要直观,类型要明确。否则模型可能为了迎合工具格式而编造参数。
4. 完整实战案例
下面我们构建一个“客服问答 Agent”,它支持两个工具:查询订单状态、计算配送时间。整体流程是:
- 用户输入问题。
- Agent 调用模型判断是否需要工具。
- 如果需要,执行工具并回传结果。
- 模型结合工具结果生成最终回答。
- 通过 FastAPI 暴露 HTTP 接口。
4.1 定义工具函数
文件路径:tools.py
import json import random from datetime import datetime, timedelta # 模拟订单数据,实际场景建议从数据库或第三方接口读取 ORDER_DATA = { "ORD20240101": { "status": "已发货", "city": "北京", "ship_date": "2024-06-01", "product": "无线鼠标" }, "ORD20240102": { "status": "待支付", "city": "上海", "ship_date": None, "product": "机械键盘" }, "ORD20240103": { "status": "已签收", "city": "广州", "ship_date": "2024-05-28", "product": "显示器" } } def query_order_status(order_id: str) -> str: """根据订单号查询订单状态""" order_id = order_id.strip().upper() if order_id in ORDER_DATA: info = ORDER_DATA[order_id] if info["status"] == "已发货": return json.dumps( { "order_id": order_id, "status": info["status"], "city": info["city"], "ship_date": info["ship_date"], "product": info["product"], }, ensure_ascii=False, ) return json.dumps( {"order_id": order_id, "status": info["status"], "product": info["product"]}, ensure_ascii=False, ) return json.dumps({"error": "order not found"}, ensure_ascii=False) def calculate_delivery_days(city: str) -> str: """根据目的城市模拟计算配送天数""" city = city.strip() base_days = { "北京": 2, "上海": 3, "广州": 4, "深圳": 3, "成都": 5, "杭州": 3, } days = base_days.get(city, 7) eta = datetime.now() + timedelta(days=days) return json.dumps( {"city": city, "delivery_days": days, "estimated_arrival": eta.strftime("%Y-%m-%d")}, ensure_ascii=False, )这里使用了random和datetime,但实际并没有用到 random,可以去掉。为了保持简单,我在这里没有使用随机数,而是按城市固定天数。你可以根据业务扩展。注意工具函数返回值是 JSON 字符串,这是为了与模型输入保持一致,也可以直接返回 dict,但字符串解析更通用。
4.2 编写 Agent 核心逻辑
文件路径:agent.py
import json from openai import OpenAI from tools import query_order_status, calculate_delivery_days class CustomerServiceAgent: def __init__(self, model_name: str = "gpt-4o-mini", base_url: str = None, api_key: str = None): self.client = OpenAI(api_key=api_key, base_url=base_url) self.model = model_name self.tools = [ { "type": "function", "function": { "name": "query_order_status", "description": "根据订单号查询订单状态,订单号格式类似 ORD20240101", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "用户的订单号" } }, "required": ["order_id"] } } }, { "type": "function", "function": { "name": "calculate_delivery_days", "description": "根据目的城市计算预计配送天数,城市为中文名", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "目的城市" } }, "required": ["city"] } } } ] def run(self, user_input: str, max_steps: int = 5) -> str: messages = [ { "role": "system", "content": ( "你是企业客服助手。你可以使用工具查询订单状态和计算配送时间。" "如果用户问题与业务无关,请礼貌拒绝回答。" "如果工具返回‘order not found’,请告诉用户订单号不存在。" "不要编造订单信息。回答尽量简洁自然。" ), }, {"role": "user", "content": user_input}, ] for step in range(max_steps): resp = self.client.chat.completions.create( model=self.model, messages=messages, tools=self.tools, tool_choice="auto", temperature=0.1, ) msg = resp.choices[0].message messages.append(msg) if msg.tool_calls: for tool_call in msg.tool_calls: func_name = tool_call.function.name args = json.loads(tool_call.function.arguments or "{}") if func_name == "query_order_status": result = query_order_status(args["order_id"]) elif func_name == "calculate_delivery_days": result = calculate_delivery_days(args["city"]) else: result = json.dumps({"error": f"unknown tool {func_name}"}) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result, }) continue return msg.content or "" return "处理超时,请稍后再试。"核心逻辑在run方法中:
- 第一次请求包含用户输入和工具定义。
- 如果模型返回
tool_calls,循环执行工具并追加tool消息。 - 如果模型没有返回
tool_calls,说明已经生成最终答案,直接返回。 - 设置
max_steps防止无限循环。
注意:不同的 API 服务对tool_choice的支持略有差别。如果你的服务不支持,可以去掉这个参数。
4.3 创建环境变量文件
文件路径:.env
OPENAI_API_KEY=sk-your-key OPENAI_BASE_URL=https://api.example.com/v1 OPENAI_MODEL_NAME=gpt-4o-mini建议不要将真实密钥提交到 Git 仓库。在本地测试时,用python-dotenv自动加载。
4.4 编写 FastAPI 服务
文件路径:app.py
import os from dotenv import load_dotenv from fastapi import FastAPI from pydantic import BaseModel from agent import CustomerServiceAgent load_dotenv() app = FastAPI(title="AI 客服助手") agent = CustomerServiceAgent( model_name=os.getenv("OPENAI_MODEL_NAME", "gpt-4o-mini"), base_url=os.getenv("OPENAI_BASE_URL"), api_key=os.getenv("OPENAI_API_KEY"), ) class ChatRequest(BaseModel): message: str class ChatResponse(BaseModel): reply: str @app.post("/chat", response_model=ChatResponse) async def chat(req: ChatRequest): reply = agent.run(req.message) return ChatResponse(reply=reply) @app.get("/health") async def health(): return {"status": "ok"}这里需要注意:
agent是全局对象,可以复用连接池。- 由于 Agent 内部调用的是阻塞式 API,所以
chat接口用async def但内部没有真正异步,生产环境建议配合线程池或异步客户端使用。 - 为了演示方便,这里省略了请求日志、错误处理和接口限流,生产环境必须补充。
4.5 运行与验证
在项目根目录执行:
uvicorn app:app --host 0.0.0.0 --port 8000服务启动后,打开另一个终端,用curl测试:
curl -X POST http://localhost:8000/chat \ -H "Content-Type: application/json" \ -d '{"message": "我的订单 ORD20240101 到哪了?"}'预期返回结果类似:
{ "reply": "您好,您的订单 ORD20240101 已经发货,商品是无线鼠标,发货日期为 2024 年 6 月 1 日。请问还有什么可以帮您?" }再测试一个需要连续使用工具的场景:
curl -X POST http://localhost:8000/chat \ -H "Content-Type: application/json" \ -d '{"message": "我的订单是 ORD20240103,发货到杭州要几天?"}'这个任务需要先查询订单信息,再计算配送时间。Agent 会自动执行两步工具调用,最后给出答案。你可以看到循环中messages的长度不断增加,这就是多步推理的过程。
5. 常见问题与排查思路
在实际开发中,最容易卡住的往往不是业务逻辑,而是模型和工具之间的协作异常。这里整理几个高频问题。
5.1 模型返回了不存在的函数名
现象:Agent 运行时抛出unknown tool或json.loads解析失败。
原因:模型根据工具描述生成了错误的函数名,或者参数格式不符合预期。
排查步骤:
- 打印
tool_call.function.name,确认模型输出。 - 检查工具描述是否明确,避免相似函数名。
- 在代码中增加兜底处理,返回错误信息给模型。
解决方案:在异常情况下,把{"error": "调用失败"}返回给模型,让模型自行调整。不要直接让程序崩溃。
5.2 上下文长度超限
现象:多轮对话后请求报错,提示maximum context length。
原因:每次工具调用的结果都追加到 messages,导致上下文超过模型上限。
解决方案:对工具返回内容做截断,例如只保留 500 字符;对较长的多轮对话做摘要或丢弃早期消息。生产系统还需要设计上下文压缩策略。
5.3 AI 幻觉导致编造订单信息
现象:用户问一个不存在的订单号,模型却生成了“订单已发货”之类的回答。
原因:模型在训练时没有见过你的订单数据,它只能依赖提示词和工具结果。如果工具没有返回有效数据,模型可能会“补全”一个答案。
解决方案:在提示词中明确说明“工具返回空或错误时,必须告知用户查询失败”;同时在工具返回中带上明确的error字段。更严格的方案是在模型回答后增加一层校验逻辑,例如使用正则检查订单号是否出现在工具结果中,如果没出现则强制替换答案。
5.4 API 请求超时
现象:外部模型接口响应慢,或偶尔超时。
原因:网络波动、模型后端负载高、请求带上了过长的上下文。
解决方案:在客户端设置超时时间,例如timeout=30;使用重试策略,但注意避免重试导致业务重复。生产环境可以引入请求队列和缓存机制。
5.5 工具调用循环无法结束
现象:Agent 反复调用同一个工具,始终不输出最终答案。
原因:模型对工具返回结果不满意,试图再次调用获取更符合预期的内容。
解决方案:设置max_steps,例如 5 步后强制返回最后一次模型输出或超时提示。同时可以在一轮工具调用后加入“系统强制结束”的提示词,不要让模型无限制尝试。
6. 最佳实践与工程建议
6.1 成本控制
大模型按 token 计费,Agent 多轮工具调用会显著增加 token 消耗。上线前必须评估:
- 限制每轮对话的最大步骤数。
- 对长工具结果进行截断或摘要。
- 对高频问题做缓存,命中缓存直接返回结果。
- 可以通过分类模型先判断问题类型,减少不必要的工具调用。
6.2 安全与权限边界
让 Agent 调用工具时要遵循最小权限原则。例如客服 Agent 只需要订单查询权限,不要给它连接内部数据库的高权限账号。涉及用户隐私时,还要做敏感信息脱敏。如果你部署本地模型,更要关注模型输出内容的安全过滤,避免产生不当内容。
6.3 日志与可观测性
Agent 的推理过程很难预测,因此必须记录完整消息链路。推荐为每个请求分配一个request_id,并记录:
- 用户输入。
- 每次模型返回的 token 数。
- 工具调用名称和参数。
- 最终回答。
这样出现问题时可以快速复现和定位。日志中不要记录完整 API Key,不要记录用户敏感信息。
6.4 模型版本与接口变更
大模型接口更新频繁,模型名称和参数可能随时调整。建议把模型名、base_url 放到配置中心或环境变量中,避免改代码。如果服务比较稳定,可以考虑将模型客户端封装成独立模块,方便将来替换供应商。
6.5 性能优化
Agent 的核心瓶颈在网络 IO。生产环境建议:
- 将模型客户端配置为连接池复用。
- 对 Web 服务增加并发控制,防止突发请求打满后端。
- 使用消息队列削峰,异步处理非实时任务。
- 如果使用本地模型部署,推荐使用 vLLM 或 TGI 等推理加速框架,它们支持连续批处理和 PagedAttention,可以有效提升吞吐。
6.6 测试策略
AI Agent 的输出具有随机性,不能只靠传统单元测试。可以先做“确定性测试”:固定模型温度为 0,对已知输入断言工具调用参数;再做“场景回归测试”:每次更新提示词后,跑一遍典型业务场景,人工或自动比较回答质量。建议把测试用例沉淀到仓库中,形成回归集。
7. 总结与学习路线
这篇教程从一个客服问答 Agent 入手,串起了 AI 工程实践中最核心的几个能力:模型接口调用、Function Calling、工具编排、Web 服务封装和故障排查。掌握这些之后,你可以再往三个方向深入学习:
- 检索增强生成(RAG),把企业知识库接入 Agent。
- 多 Agent 协作框架,例如让多个 Agent 分别承担信息检索、分析和回复生成。
- 模型部署与推理优化,包括量化、微调、以及 vLLM 等推理框架的使用。
如果你正在参与 AI 项目,建议优先关注线上稳定性,不要只沉迷于“模型效果”。一个能在生产环境稳定跑 99% 请求的简单 Agent,远好过一个演示惊艳但频繁超时的复杂系统。动手跑一遍上面这个项目,再结合自己的业务场景扩展工具函数,你会对 AI 应用开发有更实在的体感。遇到问题时,回到日志和请求链路里找答案,这比盲目调提示词更高效。收藏这篇教程,也欢迎在评论区分享你遇到的 AI Agent 工程问题,我会抽时间一起讨论。