Python实战:从零构建Agentic AI日程管理代理
2026/9/9 19:46:08 网站建设 项目流程

大家好,本篇是一篇关于 Agentic AI 工程化落地的技术笔记。过去一年,“AI 代理”“Agentic AI”频繁出现在技术圈,但网上的大量内容停留在概念层面:把 Tool Calling、ReAct、记忆、规划这些名词解释一遍,却没有演示一条从代码到可运行系统的完整链路。这篇文章会以 Python 为主语言,从零构建一个可以实际使用的日程管理 AI 代理。它会调用工具获取当前时间、添加日程、查询日程,并且具备 LLM 工具调用、状态维护、循环编排等真实 Agent 的核心能力。读完这篇文章,你能理解 Agent 的底层运作方式,能把工程代码迁移到自己的业务中,也能在遇到常见报错时快速定位问题。

1. 为什么需要 Agentic AI

1.1 传统对话应用的局限

我们平时使用的聊天机器人、智能客服,本质上是一个“输入-输出”映射系统:用户给出一段文本,模型返回一段文本。对于知识问答、文本翻译、内容改写这类任务,这种模式足够用,因为模型只需要从自己的参数或外部知识库中找到答案,然后组织成自然语言输出。

但要处理“帮我在明天下午 3 点添加一条时长 1 小时的会议日程”这种指令式任务,纯文本模型就显得力不从心。模型可以理解这句话的意图,但无法真正去操作日历系统,也不会修改你本地的数据库。更糟糕的是,很多模型为了“表现得有用”,会直接编造一个“已经帮你添加成功”的假结果,这种幻觉在业务场景中是非常危险的。

问题的根源在于:传统聊天机器人只有“嘴”,没有“手”。它不能查询外部系统,不能写入数据,不能执行命令,也不能验证自己的操作结果是否真实生效。Agentic AI 要解决的,正是这个问题。

1.2 Agentic AI 是什么

Agentic AI,也就是代理式人工智能,指的是一类能够自主完成任务的智能系统。它通常由一个或多个大语言模型作为“大脑”,通过工具调用能够感知外部环境、操作系统和数据,并通过循环机制在多次“行动-观察-再行动”的过程中逼近目标。

Agentic AI 的几个关键特征可以概括为三层:

第一是自主性。系统不只是回答用户的问题,而是承担一个完整任务,比如“整理这周所有会议并生成一份摘要”,整个过程可能需要拆解成多个子步骤。

第二是工具使用。Agent 可以调用预定义的函数、API、数据库查询,甚至执行代码。工具是 Agent 与真实世界交互的接口,没有工具的 Agent 和聊天机器人没有本质区别。

第三是循环反馈。Agent 不是一次调用模型就结束,而是把每次工具执行的结果“观察”到,再交给模型继续推理,直到得到最终结果。

这里有一个容易混淆的概念:Agentic AI 和 RAG(检索增强生成)并不冲突。RAG 的核心是把外部知识检索出来,让模型生成更准确的回答;Agent 的核心是执行动作并验证结果。Agent 内部完全可以嵌入 RAG 模块来弥补知识不足,二者是互补关系。

1.3 Agent 的典型应用场景

从实际工程角度看,Agent 的应用场景大致可以分成以下几类:

日程与邮件助理。这是最典型的入门场景,Agent 可以读取邮件、解析时间、创建日程、设置提醒,甚至协调多个参会人的时间。

数据分析助手。用户用自然语言提出问题,Agent 自动生成 SQL 查询、执行查询、读取结果、生成图表,并把结论用自然语言返回。

客服工单处理。Agent 接收用户问题后,先检索知识库,再调用工单系统查询订单状态、创建售后工单或执行退款操作。

代码生成与执行。Agent 可以生成 Python 脚本并在沙箱中执行,根据执行结果修正代码,最后交付达到预期结果的文件。

多步骤研究任务。比如“调研某行业近一年的投融资事件,整理成表格”,Agent 需要拆解检索关键词、执行多次搜索、汇总来源、清洗数据、生成报告。

这些场景的共同点是:任务的完成需要“动作”,而不只是“回答”。这也是我们为什么需要用工程手段来构建 Agent,而不是简单调用一次大模型。

2. 环境准备与整体架构

2.1 Python 环境准备

本次实战以 Python 3.10 及以上版本为例。在开始之前,先确认本机 Python 环境正常。Windows、Linux、macOS 终端都可以执行:

python --version

如果能输出类似Python 3.10.12的内容,说明环境正常。如果提示找不到命令,或者执行python --version没有输出,通常是 Python 未安装,或者安装时没有勾选“Add Python to PATH”。解决办法是重新安装 Python,并在安装向导中勾选 PATH 相关选项。安装完成后,重新打开终端再验证一次。

为了保证项目依赖不污染系统 Python,建议创建虚拟环境。在项目目录下执行:

python -m venv .venv

Windows 下激活虚拟环境:

.venv\Scripts\activate

Linux / macOS 下激活虚拟环境:

source .venv/bin/activate

激活后,命令行提示符会显示(.venv)。后续安装依赖和运行代码都在虚拟环境中操作。如果你使用 VSCode,还需要在解释器选择中指向当前项目下的.venv路径,否则 VSCode 的终端可能不会自动激活虚拟环境;PyCharm 则可以在项目设置中直接选择虚拟环境作为项目解释器。

2.2 模型接入:本地模型与 OpenAI 兼容接口

构建 Agent 需要一个支持工具调用的 LLM。当前主流的 LLM 服务,无论是 OpenAI、Anthropic,还是各类国产大模型,大多提供了 OpenAI 兼容的 HTTP 接口。这使得我们只需要写一套客户端代码,通过更换base_urlapi_key就能切换不同模型服务。

为了便于演示,并且保证你在没有外网 API Key 的情况下也能跑通,本文默认使用 Ollama 本地模型。Ollama 是常见的本地模型运行工具,它启动后会在本机提供一个 OpenAI 兼容接口,默认地址一般是http://localhost:11434/v1。使用前需要先拉取一个支持工具调用的模型,例如:

ollama pull qwen2.5:7b

拉取完成后,可以通过ollama list查看本地已有的模型。如果你的电脑配置较低,也可以尝试qwen2.5:3bllama3.1:8b等更小的模型。需要说明的是,模型参数越小,工具调用能力通常越弱,出现“不调用工具直接编答案”的概率也越高,这是正常现象。

如果你希望直接使用 OpenAI 官方接口,只需要把代码中的base_url改为官方地址,api_key填你自己的 Key,model改为实际使用的模型名即可。由于不同模型服务在tools参数的具体格式上可能存在细小差异,示例代码以 OpenAI 兼容协议作为基准,遇到差异时以你使用的模型官方文档为准。

2.3 Agent 整体架构

在本项目的简化架构中,核心角色只有三个:LLM、Agent 编排层、工具层。我们可以用一个简图来表示:

用户输入 ↓ ┌──────────────┐ │ LLM 大模型 │ └──────┬───────┘ 调用工具 or 直接回答 ↓ ┌──────────────┐ │ 工具函数层 │ │ get_current_time │ add_event │ list_events │ └──────┬───────┘ ↓ 执行结果返回给模型 ↓ 循环,直到模型给出最终答案

LLM 负责推理和决策,工具层负责真正执行动作,Agent 编排层则负责维护消息历史、判断是否需要继续调用工具、解析工具参数、调用工具并把结果回传给模型。生产环境中还会有记忆存储、规划器、评估器、权限控制等模块,但核心循环就是这个结构。

3. Agent 核心原理拆解

3.1 工具:让模型拥有“手脚”

在 Agent 系统中,工具本质上就是一个普通函数。为了让模型知道“有哪些工具可用、每个工具需要什么参数”,我们需要把函数描述成模型能读懂的 JSON Schema。

以“添加日程”为例,工具描述大致如下:

{ "type": "function", "function": { "name": "add_event", "description": "添加一条日程", "parameters": { "type": "object", "properties": { "event_date": { "type": "string", "description": "日期,格式 YYYY-MM-DD" }, "start_time": { "type": "string", "description": "开始时间,格式 HH:MM" }, "title": { "type": "string", "description": "日程标题" }, "duration_minutes": { "type": "integer", "description": "时长,单位分钟,默认 60" } }, "required": ["event_date", "start_time", "title"] } } }

这段描述对模型至关重要。模型并不知道你的 Python 函数长什么样,它只能通过namedescriptionparameters来理解这个工具。描述写得越清晰,模型调用工具的准确率就越高。

比如description里如果写“添加日程”,模型可能不清楚日期该怎么填,甚至会把“明天”当作合法值直接填入 JSON。如果描述里明确写了“日期,格式 YYYY-MM-DD”,模型就会在调用工具前先把“明天”转换成具体的日期字符串。这一点在实际使用中非常影响体验。

3.2 工具调用机制

支持工具调用的模型,在生成回复时可能会出现两种结果。第一种是普通文本回复,这表示模型认为不需要调用任何工具。第二种是包含tool_calls字段的回复,里面有一个或多个“工具调用请求”,每个请求包含函数名和一个 JSON 字符串参数。

需要特别强调的是:模型只是“建议”调用某个工具,真正执行工具函数的是我们的 Python 代码。框架拿到tool_calls后,要做到四件事:

  1. 解析出函数名和参数 JSON;
  2. 在工具注册表中找到对应的 Python 函数;
  3. 执行函数并拿到结果;
  4. 把执行结果以role: "tool"的消息追加到对话历史中,再继续调用模型。

执行结果不能简单打印到控制台就结束,必须回传给模型。因为模型需要“观察”到工具执行结果,才能继续推理。这个“工具结果回传”的步骤,是很多人写 Agent 时最容易漏掉的。

3.3 ReAct 循环

ReAct 的全称是 Reasoning + Acting,即“思考-行动-观察”循环。ReAct 是当前 Agent 系统中最基础、也最常用的编排模式。一次完整的 ReAct 循环是这样的:

  1. 模型根据用户输入和已有的消息历史进行推理;
  2. 如果模型认为需要外部信息或需要执行动作,就输出一个tool_calls
  3. Agent 执行对应的工具函数,得到观察结果;
  4. 把观察结果追加到历史消息中,再次调用模型;
  5. 重复以上过程,直到模型不再输出tool_calls,而是给出最终回答。

听起来不复杂,但工程上有几个细节必须注意。

首先,必须设置最大迭代次数。模型在复杂任务中可能陷入工具调用的死循环,比如反复调用同一个工具,或者两个工具来回调用。设置一个max_iterations,比如 8 次,能防止系统无限消耗 token。

其次,每一步的输入输出都要记录。无论是排查问题还是优化 Agent,日志都是最直接的依据。后面在最佳实践章节里会展开讲。

最后,工具执行结果要足够明确。理想的返回值应该是短小、结构化、包含关键信息的字符串。如果工具返回一段冗长的堆栈或大量噪声,模型很容易被带偏。

3.4 记忆与状态管理

Agent 的记忆一般分为短期记忆和长期记忆。

短期记忆就是对话历史,也就是发送给模型的消息列表。多轮对话时,之前的用户输入、模型回答、工具调用和工具结果都会保留在messages数组中。短期记忆决定了 Agent 能否理解上下文中的引用关系。比如用户先问“今天有什么日程”,再问“把第一个改到下午两点”,Agent 必须能从上一次的工具调用结果中找到“第一个”指的是什么。

长期记忆则用来保存跨会话的信息,常见实现有本地文件、数据库、向量数据库。在后面的日程管理示例中,events.json文件就是长期存储,它保存了日程数据,使 Agent 在重启后仍能查询到之前添加的日程。

这里有两种容易踩坑的情况。第一种是不保存工具结果,导致模型在下一轮回复时回忆不出之前的查询结果;第二种是历史消息无限增长,当上下文超出模型窗口时,请求直接失败。生产系统通常会对历史做截断或摘要压缩,只保留最近若干轮核心信息。

4. 完整实战:用 Python 构建一个日程管理 AI 代理

4.1 项目结构

在开始写代码之前,先规划项目结构。为了让逻辑更清晰,我们把代码拆成四个文件,每个文件只负责一个层面:

agentic_demo/ ├── main.py # 命令行入口 ├── agent.py # Agent 编排核心 ├── llm.py # LLM 调用封装 ├── tools.py # 工具函数层 ├── requirements.txt # 项目依赖 └── events.json # 运行时自动生成的日程数据文件

这样的结构在小型项目中已经足够清晰:tools.py是模型可以调用的函数集合,llm.py屏蔽底层 HTTP 请求细节,agent.py负责编排循环,main.py只负责和用户交互。

4.2 工具函数层

文件路径:agentic_demo/tools.py

import json import os from datetime import datetime EVENT_FILE = os.path.join(os.path.dirname(__file__), "events.json") def load_events(): if not os.path.exists(EVENT_FILE): return [] with open(EVENT_FILE, "r", encoding="utf-8") as f: return json.load(f) def save_events(events): with open(EVENT_FILE, "w", encoding="utf-8") as f: json.dump(events, f, ensure_ascii=False, indent=2) def get_current_time(): """获取当前本地时间。""" return datetime.now().strftime("%Y-%m-%d %H:%M:%S") def add_event(event_date, start_time, title, duration_minutes=60): """添加一条日程。""" events = load_events() new_id = max([event["id"] for event in events], default=0) + 1 event = { "id": new_id, "date": event_date, "start": start_time, "duration_minutes": duration_minutes, "title": title, } events.append(event) save_events(events) return ( f"日程添加成功:ID={new_id},日期={event_date}," f"开始时间={start_time},标题={title},时长={duration_minutes}分钟" ) def list_events(event_date=None): """查询日程,可按日期过滤。""" events = load_events() if event_date: events = [event for event in events if event["date"] == event_date] if not events: return "没有找到任何日程。" lines = ["日程列表:"] for event in events: lines.append( f"- [{event['date']} {event['start']}] " f"{event['title']}({event['duration_minutes']}分钟)" ) return "\n".join(lines)

这段代码有三个设计细节值得注意。

第一,每个工具函数都返回字符串。大语言模型的输入是文本,返回字符串能直接放进content字段,不需要额外序列化。如果返回一个 Python 字典,还需要在 Agent 编排层做json.dumps,增加出错概率。

第二,文件读写使用了相对稳定的路径。os.path.dirname(__file__)获取当前文件所在目录,这样无论从哪个目录运行脚本,events.json都能被正确定位。

第三,工具函数不做任何权限判断。演示版本为了简单,任何人都可以添加和查询日程。生产环境里,工具层必须增加用户身份校验和越权检查。

4.3 LLM 调用封装

文件路径:agentic_demo/llm.py

import requests class ChatClient: def __init__( self, base_url="http://localhost:11434/v1", api_key="ollama", model="qwen2.5:7b", timeout=120, ): self.base_url = base_url.rstrip("/") self.api_key = api_key self.model = model self.timeout = timeout def chat(self, messages, tools=None, temperature=0.3): payload = { "model": self.model, "messages": messages, "temperature": temperature, } if tools: payload["tools"] = tools resp = requests.post( f"{self.base_url}/chat/completions", json=payload, headers={"Authorization": f"Bearer {self.api_key}"}, timeout=self.timeout, ) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]

这里我把模型调用封装为一个ChatClient类,后续 Agent 编排层只依赖client.chat()方法,而不关心底层 HTTP 细节。这样做的直接好处是:测试时可以很方便地用一个“假 Client”替换真实网络请求,后面测试策略里会再提。

你可能注意到,代码里请求的是/chat/completions,这是 OpenAI 兼容接口的统一路径。Ollama、vLLM、各类云厂商的兼容网关都实现了这个入口。Authorization请求头对 Ollama 来说并不强制,但保留它可以让我们无缝切换到其他需要 Key 的服务。

4.4 Agent 编排核心

文件路径:agentic_demo/agent.py

import json from llm import ChatClient from tools import add_event, get_current_time, list_events TOOL_SCHEMAS = [ { "type": "function", "function": { "name": "get_current_time", "description": "获取当前本地时间,返回格式为 YYYY-MM-DD HH:MM:SS", "parameters": { "type": "object", "properties": {}, }, }, }, { "type": "function", "function": { "name": "add_event", "description": "添加一条日程。日期格式必须为 YYYY-MM-DD,时间格式必须为 HH:MM。", "parameters": { "type": "object", "properties": { "event_date": { "type": "string", "description": "日期,格式 YYYY-MM-DD", }, "start_time": { "type": "string", "description": "开始时间,格式 HH:MM", }, "title": { "type": "string", "description": "日程标题", }, "duration_minutes": { "type": "integer", "description": "时长,单位分钟,默认 60", }, }, "required": ["event_date", "start_time", "title"], }, }, }, { "type": "function", "function": { "name": "list_events", "description": "查询日程。可按日期过滤,日期可选,格式 YYYY-MM-DD。", "parameters": { "type": "object", "properties": { "event_date": { "type": "string", "description": "可选,日期,格式 YYYY-MM-DD", }, }, }, }, }, ] TOOL_FUNCTIONS = { "get_current_time": get_current_time, "add_event": add_event, "list_events": list_events, } class Agent: def __init__(self, client): self.client = client self.history = [] self.max_iterations = 8 def system_prompt(self): return ( "你是一个日程管理 AI 助手。当你需要获知当前时间、添加日程或查询日程时," "你必须调用对应工具,不要凭记忆编造日程数据。" "所有日期使用 YYYY-MM-DD 格式,时间使用 HH:MM 格式。" ) def run(self, user_input): if not self.history: self.history.append({"role": "system", "content": self.system_prompt()}) self.history.append({"role": "user", "content": user_input}) for step in range(1, self.max_iterations + 1): message = self.client.chat(self.history, tools=TOOL_SCHEMAS) self.history.append(message) if not message.get("tool_calls"): final_answer = message.get("content") or "(模型没有返回内容)" print(f"\n[Agent] 最终回答:{final_answer}") return final_answer for tool_call in message["tool_calls"]: function = tool_call.get("function", {}) name = function.get("name", "") try: arguments = json.loads(function.get("arguments") or "{}") except json.JSONDecodeError: arguments = {} print(f"[Step {step}] 调用工具:{name},参数:{arguments}") result = self.execute_tool(name, arguments) print(f"[Step {step}] 工具返回:{result}") self.history.append( { "role": "tool", "tool_call_id": tool_call.get("id"), "content": result, } ) return "已达到最大迭代次数,没有生成最终答案。" def execute_tool(self, name, arguments): fn = TOOL_FUNCTIONS.get(name) if fn is None: return f"错误:未知工具 {name}" try: return fn(**arguments) except TypeError as exc: return f"错误:工具参数不正确:{exc}" except Exception as exc: return f"错误:工具执行失败:{exc}"

agent.py是整个项目最核心的文件。

先看system_prompt。这里明确告诉模型:“必须调用工具,不要凭记忆编造日程数据”。这行提示非常关键,因为很多情况下,模型宁可凭记忆编造一个“已添加成功”的答复,也不愿意调用工具。在工具调用能力较弱的模型上,这种提示词约束能明显提升工具调用率。

再看run方法的循环。每次循环先调用一次模型,然后把模型返回的message追加到历史中。如果message不包含tool_calls,说明模型认为任务已经完成,可以直接输出最终回答。否则,就遍历所有工具调用,逐个执行并把结果以role: "tool"追加回去。

execute_tool方法做了参数异常保护。无论模型给的参数多离谱,工具执行失败都会返回一个错误字符串,而不是让 Python 进程崩溃。这个错误字符串会传给模型,模型通常会根据错误信息修正参数,再尝试一次调用。

4.5 运行与验证

文件路径:agentic_demo/main.py

from agent import Agent from llm import ChatClient def main(): client = ChatClient( base_url="http://localhost:11434/v1", api_key="ollama", model="qwen2.5:7b", ) agent = Agent(client) print("日程管理 AI 代理已启动。") print("你可以说:现在几点?/ 帮我添加明天下午3点的会议 / 明天的日程有哪些?") print("输入 /reset 清空对话历史,输入 exit 退出。\n") while True: user_input = input("你:").strip() if user_input.lower() in ("exit", "quit"): print("再见!") break if user_input == "/reset": agent.history = [] print("对话历史已清空。") continue if not user_input: continue print("======================") agent.run(user_input) print("======================\n") if __name__ == "__main__": main()

运行前需要先启动 Ollama 服务。大多数情况下,Ollama 安装后会作为后台服务自动运行;如果没有,可以在终端执行ollama serve手动启动。确认服务正常后,再在项目目录运行:

python main.py

下面是一次典型的对话过程。不同模型输出会略有差异,但流程一致:

日程管理 AI 代理已启动。 你可以说:现在几点?/ 帮我添加明天下午3点的会议 / 明天的日程有哪些? 输入 /reset 清空对话历史,输入 exit 退出。 你:现在几点? ====================== [Step 1] 调用工具:get_current_time,参数:{} [Step 1] 工具返回:2025-03-12 14:30:22 [Agent] 最终回答:当前时间是 2025年3月12日14点30分22秒。 ======================

第二个问题会复杂一些。当用户说“帮我添加明天下午 3 点的项目例会,时长 1 小时”,模型很可能需要先调用get_current_time或直接通过系统提示中的日期推断出“明天”的具体日期,然后调用add_event添加日程:

你:帮我添加明天下午3点的项目例会,时长1小时 ====================== [Step 1] 调用工具:get_current_time,参数:{} [Step 1] 工具返回:2025-03-12 14:35:10 [Step 2] 调用工具:add_event,参数:{'event_date': '2025-03-13', 'start_time': '15:00', 'title': '项目例会', 'duration_minutes': 60} [Step 2] 工具返回:日程添加成功:ID=1,日期=2025-03-13,开始时间=15:00,标题=项目例会,时长=60分钟 [Agent] 最终回答:已经帮你添加好了明天下午3点的项目例会,时长1小时。 ======================

第三个问题“明天有什么安排”则演示了日期过滤查询:模型先算出明天的日期,再调用list_events,最后根据返回结果组织回答。如果你看到 Agent 能连续调用两个工具完成一个任务,说明整个 ReAct 循环已经跑通了。

5. 编排模式与生产化改造

5.1 ReAct、Plan-and-Execute、Multi-Agent 对比

本文的日程管理示例采用的是最基础的 ReAct 模式。它的优点是实现简单、逻辑透明、可控性强,每一步都能追踪;缺点是每一步都要调用一次模型,在复杂任务中延迟和 token 成本都比较高。

面向真实生产环境,还有两种更高层的编排模式值得了解。

Plan-and-Execute 模式适合“步骤明确、任务较重”的场景。它的思路是让模型先制定一个计划,比如“第一步搜索资料,第二步清洗数据,第三步生成报告”,然后按照计划逐步执行。与 ReAct 相比,它减少了模型在每一步之间反复决策的次数,整体效率更高。但计划一旦脱离实际,就需要额外的纠错机制。

Multi-Agent 模式适合“多个专业角色协作”的场景。比如一个 Agent 负责分析需求,另一个 Agent 负责写代码,第三个 Agent 负责代码审查。每个 Agent 可以持有不同的系统提示词和工具集合,它们之间通过消息传递进行协作。这种模式职责清晰、可扩展性强,但通信与协调成本也明显更高。

编排模式适用场景优点缺点
ReAct单 Agent、需要多次工具调用的任务实现简单,可控性强每步都调用 LLM,延迟和成本高
Plan-and-Execute步骤明确,任务较重减少中间决策,先计划再执行计划可能脱离实际,需要纠错机制
Multi-Agent多个专业角色协作职责分离,组件可复用通信复杂,整体成本更高

生产系统通常不会只使用一种模式。常见的架构是:主 Agent 先做任务规划,然后把子任务分发给多个子 Agent,子 Agent 内部再使用 ReAct 循环完成具体动作。我们本文写的日程管理 Agent,是整个架构中最基础的一个“执行单元”。

5.2 从命令行到 FastAPI 服务

命令行交互适合本地调试,但真实业务通常需要通过 HTTP 接口暴露 Agent 能力。这里用 FastAPI 做一个简单的服务化包装。

先安装依赖:

pip install fastapi uvicorn pydantic

文件路径:agentic_demo/server.py

from fastapi import FastAPI from pydantic import BaseModel from agent import Agent from llm import ChatClient app = FastAPI() client = ChatClient( base_url="http://localhost:11434/v1", api_key="ollama", model="qwen2.5:7b", ) agent = Agent(client) class ChatRequest(BaseModel): message: str @app.post("/chat") def chat(req: ChatRequest): result = agent.run(req.message) return {"reply": result}

启动服务:

uvicorn server:app --host 0.0.0.0 --port 8000

启动后,可以用curl测试:

curl -X POST http://localhost:8000/chat \ -H "Content-Type: application/json" \ -d '{"message": "现在几点?"}'

这个版本能跑,但不适合直接上生产,因为它存在两个明显问题。

第一是历史共享。所有请求共用同一个agent实例和同一份history,用户 A 说了一句话,用户 B 会看到;第二个问题则是并发。FastAPI 默认异步处理请求,而我们的agent.run是同步阻塞方法,多个请求同时进入时,historyevents.json会发生数据竞争。下一节给出会话隔离的改进思路。

5.3 多会话状态管理

生产环境中,每个用户应该有独立的会话历史。最简单的方式是用内存字典保存多个 Agent 实例,每个session_id对应一个独立的Agent

from fastapi import FastAPI from pydantic import BaseModel from agent import Agent from llm import ChatClient app = FastAPI() client = ChatClient( base_url="http://localhost:11434/v1", api_key="ollama", model="qwen2.5:7b", ) agents = {} class ChatRequest(BaseModel): session_id: str message: str @app.post("/chat") def chat(req: ChatRequest): if req.session_id not in agents: agents[req.session_id] = Agent(client) agent = agents[req.session_id] result = agent.run(req.message) return {"reply": result}

这样做解决了历史串线问题,但内存字典在服务重启后会丢失,多 worker 部署时不同进程之间也无法共享agents。生产环境更常见的做法是把会话消息存储到 Redis 或数据库中,每次请求从存储中加载历史,调用模型后再把更新后的历史写回去。这样 Agent 实例可以做到无状态,便于水平扩展。

6. 常见问题与排查思路

Agent 系统由模型、编排代码、工具函数、外部服务共同组成,出问题的环节非常多。下面整理一份高频问题排查表。

问题现象常见原因解决思路
python --version没有输出Python 未安装或未加入 PATH重新安装 Python,勾选 Add to PATH,重启终端
pip install报错网络不稳定或权限不足使用国内镜像源,或加--user参数重试
请求 Ollama 时 ConnectionErrorOllama 服务未启动或端口不对运行ollama serve,检查http://localhost:11434是否可访问
报错model not found本地没有对应模型先执行ollama pull qwen2.5:7b
模型直接回答“已添加”但不调用工具模型太弱或提示词约束不够在 system prompt 中强制要求调用工具,换支持 tool calling 的模型
argumentsJSON 解析失败模型返回的 arguments 不是合法 JSON用异常捕获让模型重试,或改用结构化输出功能
Agent 反复调用同一个工具不结束没有设置迭代上限,或工具结果不明确增加max_iterations,优化工具返回信息的明确性
多轮对话后请求超时历史消息太长,超出模型窗口截断历史、做摘要,或只保留最近若干轮
FastAPI 多请求下数据错乱全局共享 Agent 实例和 history按 session_id 隔离 Agent 实例,或使用 Redis 保存会话状态
本地模型经常不按参数格式调用小模型工具调用能力较弱换更大参数模型,或在工具描述中给出调用示例

如果遇到 Agent 行为不符合预期,建议按下面顺序排查。

第一步,先单独验证工具函数。直接写一个小脚本 importtools.py,调用add_eventlist_events,确认函数本身没有问题。很多 Agent 故障其实是“工具函数有 bug”,而不是模型的问题。

第二步,检查模型返回的原始消息。在agent.pyclient.chat之后打印完整message,看模型到底是没调用工具,还是调用了工具但参数不对。这一步能快速区分是“模型决策问题”还是“代码解析问题”。

第三步,检查工具结果是否成功回传给模型。如果role: "tool"消息没有正确追加到history,模型在下一步就看不到观察结果,会出现重复调用或幻觉回答。

第四步,确认模型是否支持tools参数。部分模型虽然声称兼容 OpenAI 接口,但实际不支持原生tool_calls,这会导致模型忽略tools直接返回普通文本。这时需要在提示词中把工具描述以文本形式告诉模型,并让模型输出固定格式的 JSON,再由代码解析执行。

7. 最佳实践与工程建议

7.1 工具设计原则

工具是 Agent 能力的边界。一个设计良好的工具应该满足以下原则。

第一,职责单一。每个工具只做一件事。一个“更新日程”的工具不要同时负责“删除日程”,否则模型在描述模糊时很难判断该不该调用它。

第二,参数要少且类型明确。参数越少,模型生成 JSON 时越不容易出错。能用字符串的尽量用字符串,不要设计嵌套 JSON 对象作为参数。嵌套结构对模型推理能力要求很高,小模型经常填错。

第三,返回值要面向模型。工具返回的字符串尽量包含“成功失败状态”“关键 ID”“核心数据”以及“下一步建议”。比如删除失败时返回“删除失败,原因是日程不存在,请先查询日程ID”,模型就能顺着这个信息继续引导用户。

第四,工具必须设置超时和异常捕获。调用外部 API、执行数据库查询时,网络抖动或超时是常态。工具内部要有try-except,并返回可读的错误信息,而不是让异常直接抛到 Agent 编排层。

第五,幂等性。如果工具可以被重复执行,重复执行的副作用要尽量小。例如创建一个支付订单的工具不能因为网络重试而创建两笔订单,这需要业务侧设计幂等键。

7.2 安全边界

Agent 一旦被允许调用工具,风险边界就从“模型说什么话”升级到了“系统执行什么动作”。这里必须把安全放在第一位。

最小权限原则是底线。如果 Agent 只需要查询数据库,那数据库账号就应该只授予SELECT权限,而不是rootDBA。如果 Agent 需要修改数据,也只在必要时授予对应表的UPDATE权限。对删除、清空、转账、审批这类敏感操作,代码层必须加入二次确认机制,不能只靠一句自然语言就触发删除。

工具参数要做好校验。比如list_events接受一个event_date参数,必须在代码里用datetime.strptime校验格式,而不是直接传给文件系统或 SQL。任何来自模型输出的参数都应当被当作不可信输入处理,坚决不能拼接 SQL 或 Shell 命令。

最后,一定要记录审计日志。谁在什么时间,通过什么会话,输入了什么 Prompt,最终触发了哪些工具,传入什么参数,执行结果如何。这些日志不仅是排查问题的依据,也是安全审查的重要证据。

7.3 可观测性与日志

Agent 系统比普通 Web 接口复杂得多,因为一个用户请求背后可能有多轮模型调用和工具调用。日志记录不完整,排错会非常痛苦。

建议每轮循环至少记录以下信息:唯一请求 ID 或 trace_id、模型名称、输入 token 数和输出 token 数、工具名称、工具参数、工具耗时、工具结果、是否异常、是否重试。把这些信息打印成结构化日志,方便后续接入 ELK、Loki 等日志平台。

这里有一个容易忽略的点:模型的message要原样保存,不要只保留解析后的参数。因为同样的参数可能由不同的模型生成,保存原始输出能帮助你在升级模型时做对比分析。

7.4 模型选型与成本控制

模型选型没有一个固定答案,但可以按照任务复杂度来粗略判断。

简单任务,比如单次工具调用、固定格式抽取,可以选择参数较小的模型,响应快、成本低。复杂任务,比如多步推理、多工具协作、长文档理解,建议使用能力更强的大模型。本地小模型适合隐私敏感或离线场景,但对复杂工具调用的能力明显弱于顶级闭源模型。

成本控制的核心是减少无效调用。一些请求其实不需要进入模型。比如查询最近日程,如果用户的问题非常固定,可以直接走规则匹配,命中后执行工具返回结果,完全不需要调用 LLM。再比如高频的重复问题,可以用缓存命中直接返回。对历史消息做摘要压缩也能显著减少上下文长度,降低 token 消耗。

7.5 测试策略

普通单元测试不够,Agent 系统还需要额外的集成测试。这里给出一个分层思路。

工具层做单元测试。直接给参数调用函数,断言返回值包含预期内容,同时覆盖异常参数和边界条件。比如add_event传入空标题、错误日期格式时,应当返回可读错误信息而不是抛出异常。

Agent 层做 Mock 测试。不要让测试代码依赖真实网络请求,而是写一个假 Client 类,手动返回带tool_calls的消息和最终回答,验证run方法的循环逻辑是否正确。这样每次运行测试都不会产生模型调用费用,也不会因为网络问题导致测试不稳定。

集成测试用真实模型但控制用例数量。准备三类用例:一类是不需要调用工具的直接回答场景,一类是只需一次工具调用的场景,一类是需要连续两次以上工具调用的场景。每次升级模型或修改提示词后,都跑一遍这些用例,对比最终输出是否仍然满足预期。

8. 写在最后:从 Demo 到真实 Agent

如果你完整跟完了上面的实战,会发现构建一个 Agent 的最小闭环其实不复杂:定义工具、接入模型、编排循环、持久化状态。这个闭环是所有复杂 Agent 系统的地基。

接下来可以做的事情有三件。第一,把你手头的具体业务抽象成工具函数,工具越多、描述越准确,Agent 能处理的任务范围就越广。第二,把 Agent 在开发过程中暴露出来的典型失败案例沉淀成测试用例,防止模型升级后行为回归。第三,尽快给系统加上可观测性指标,用真实的调用数据去评估当前模型的工具调用成功率,再根据数据决定什么时候需要换模型、什么时候需要调整提示词。

Agentic AI 的工程化不是一蹴而就,真正难的不是模型,而是如何设计可靠的工具边界、清晰的循环策略和可维护的系统结构。希望这篇笔记可以成为你构建真实 AI 代理的起点,也期待你自己动手把第一个 Agent 跑起来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询