Agent 开发实战(一):从零实现一个最小可用 Agent
Agent 开发实战(一):从零实现一个最小可用 Agent
很多教程一上来就讲"接数据库、接向量库、接一堆工具、做多 Agent 编排",但 Agent 到底是什么、核心循环怎么跑,反而讲不清。本篇只做一件事:用一个工具、不到 100 行代码,让你真正跑通一个 Agent,并讲透它为什么这样设计。后续(二)~(九)会在这个最小骨架上,逐步叠加多源数据、工具编排、记忆、前端、RAG、多 Agent、可观测性与工程化。
系列地图:(一)最小 Agent ← 本篇 | (二)多源数据接入 | (三)工具系统深入 | (四)记忆与上下文 | (五)流式与前端集成 | (六)RAG 进阶 | (七)多 Agent 协作 | (八)可观测性与评估 | (九)工程化部署与端到端案例。逻辑是「地基 → 接数据 → 工具工程化 → 记忆 → 前端 → 检索 → 多 Agent → 观测 → 部署」,每篇都基于前一篇叠加一层能力,但共享同一套"模型决策、工具执行"的底座。
1. 什么是 Agent
狭义地说,一个 Agent = 一个会"自己决定调用什么工具"的 LLM 循环。它和普通的"LLM 问答"区别在于:
- 普通问答:你问 → 模型答。模型靠训练记忆作答,容易编造,且拿不到实时/私有数据。
- Agent:你问 → 模型决定调用工具取数 → 工具返回 → 模型基于真实数据作答。
三个组成部件:
- 规划(Planning):模型读懂问题,拆成"要调哪些工具、传什么参数"。
- 工具(Tools):一段被严格约束的代码,负责真正去取数 / 执行。
- 循环(Loop):把"模型决策 → 工具执行 → 结果回灌"反复跑,直到模型认为可以作答。
记忆(Memory)也是常见部件,但本篇先不讲——它是后面(四)的主题。先把"会调工具的循环"跑通最关键。
2. 核心原则:模型只决策,工具才执行
这是整系列的铁律,现在就钉死:
- 模型永远不直接碰数据、不直接执行代码。它只输出"我要调
get_order_stats,参数是status=refunded"。 - 工具才是真正干活的:查库、读文件、算数,全在工具里完成,工具的输出再回灌给模型。
- 好处:可控、可审计、可加护栏。模型哪怕"想搞破坏",工具层根本没提供那个能力。
这条原则看着简单,却是后面对接数据库、表格、文档、生信文件时"不出事"的根本原因。
3. 最小实现:一个工具就够
我们用一个能离线跑的例子:工具 get_order_stats(status) 返回一组模拟的订单统计。不依赖任何外部服务,复制即可运行。
# agent_minimal.py
import json, os
from openai import OpenAI
client = OpenAI(api_key=os.getenv("LLM_API_KEY"), base_url=os.getenv("LLM_BASE_URL"))
# ---------- 工具实现(真正干活的代码,模型碰不到)----------
def get_order_stats(status: str) -> dict:
"""按状态返回订单统计(此处用模拟数据,真实场景换成查库)。"""
mock = {
"paid": {"count": 1280, "amount": 356_000},
"refunded": {"count": 73, "amount": 21_500},
"shipped": {"count": 902, "amount": 268_300},
}
if status not in mock:
return {"error": f"unknown status: {status}"}
return mock[status]
# ---------- 工具声明(告诉模型有哪些工具、参数怎么传)----------
TOOLS = [{
"type": "function",
"function": {
"name": "get_order_stats",
"description": "查询某状态下的订单数量与金额,用于经营分析。",
"parameters": {
"type": "object",
"properties": {
"status": {"type": "string",
"enum": ["paid", "refunded", "shipped"],
"description": "订单状态"}
},
"required": ["status"]
}
}
}]
# ---------- 工具分发(模型只出名字和参数,这里才真正调用)----------
def dispatch(name, args):
if name == "get_order_stats":
return get_order_stats(**args)
return {"error": f"unknown tool: {name}"}
# ---------- 核心循环 ----------
def run_agent(question: str, max_turns: int = 5) -> str:
messages = [
{"role": "system", "content": "你是订单分析助手,只能用工具获取真实数据,禁止编造。中文回答。"},
{"role": "user", "content": question},
]
for _ in range(max_turns):
resp = client.chat.completions.create(
model=os.getenv("LLM_MODEL", "gpt-4o-mini"),
messages=messages, tools=TOOLS, tool_choice="auto")
msg = resp.choices[0].message
if not msg.tool_calls: # 模型认为可以直接作答
return msg.content
messages.append(msg)
for call in msg.tool_calls: # 否则执行它决定的工具
args = json.loads(call.function.arguments)
result = dispatch(call.function.name, args)
messages.append({
"role": "tool", "tool_call_id": call.id,
"content": json.dumps(result, ensure_ascii=False)})
return "(已达到最大轮数,请拆分问题)"
if __name__ == "__main__":
print(run_agent("已退款的订单有多少?涉及金额多大?"))
运行后模型会:① 决定调用 get_order_stats(refunded);② 我们执行工具拿到 {"count": 73, "amount": 21500};③ 模型基于这组真实数字作答。全程没有一行数据是被模型编出来的。
4. 这个最小版的三个短板
能跑,但离生产很远——也正是后面几篇要补的:
- 只接了一个工具。真实后台数据散在数据库、表格、文档、生信文件里,(二)会把它扩成多源接入。
- 工具调用是"原始"的:串行执行、出错直接崩、没有权限区分、没有缓存。一旦工具变多,(三)工具系统深入会把这些做扎实。
- 没有记忆与护栏:每次对话从零开始,也缺审计与限流——分别在(四)(八)(九)解决。
5. 小结
Agent 不神秘,本质就是"LLM + 工具 + 循环",关键是把"决策"和"执行"切开:模型只说要调什么,工具才是干活的那层。本篇用单个工具跑通了这条链路,后面每篇都在它之上叠加一层能力,但都守住"模型只决策、工具才执行"这条线。
下一篇(二)《多源后台数据接入》:我们把这个最小 Agent 接到真实的数据库、表格、文档与生信文件上,让它能回答跨源分析问题。