Agent 开发实战(一):从零实现一个最小可用 Agent

Moryu 4 阅读 agent

Agent 开发实战(一):从零实现一个最小可用 Agent

很多教程一上来就讲"接数据库、接向量库、接一堆工具、做多 Agent 编排",但 Agent 到底是什么、核心循环怎么跑,反而讲不清。本篇只做一件事:用一个工具、不到 100 行代码,让你真正跑通一个 Agent,并讲透它为什么这样设计。后续(二)~(九)会在这个最小骨架上,逐步叠加多源数据、工具编排、记忆、前端、RAG、多 Agent、可观测性与工程化。

系列地图:(一)最小 Agent ← 本篇 | (二)多源数据接入 | (三)工具系统深入 | (四)记忆与上下文 | (五)流式与前端集成 | (六)RAG 进阶 | (七)多 Agent 协作 | (八)可观测性与评估 | (九)工程化部署与端到端案例。逻辑是「地基 → 接数据 → 工具工程化 → 记忆 → 前端 → 检索 → 多 Agent → 观测 → 部署」,每篇都基于前一篇叠加一层能力,但共享同一套"模型决策、工具执行"的底座。

1. 什么是 Agent

狭义地说,一个 Agent = 一个会"自己决定调用什么工具"的 LLM 循环。它和普通的"LLM 问答"区别在于:

  • 普通问答:你问 → 模型答。模型靠训练记忆作答,容易编造,且拿不到实时/私有数据。
  • Agent:你问 → 模型决定调用工具取数 → 工具返回 → 模型基于真实数据作答。

三个组成部件:

  1. 规划(Planning):模型读懂问题,拆成"要调哪些工具、传什么参数"。
  2. 工具(Tools):一段被严格约束的代码,负责真正去取数 / 执行。
  3. 循环(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. 这个最小版的三个短板

能跑,但离生产很远——也正是后面几篇要补的:

  1. 只接了一个工具。真实后台数据散在数据库、表格、文档、生信文件里,(二)会把它扩成多源接入。
  2. 工具调用是"原始"的:串行执行、出错直接崩、没有权限区分、没有缓存。一旦工具变多,(三)工具系统深入会把这些做扎实。
  3. 没有记忆与护栏:每次对话从零开始,也缺审计与限流——分别在(四)(八)(九)解决。

5. 小结

Agent 不神秘,本质就是"LLM + 工具 + 循环",关键是把"决策"和"执行"切开:模型只说要调什么,工具才是干活的那层。本篇用单个工具跑通了这条链路,后面每篇都在它之上叠加一层能力,但都守住"模型只决策、工具才执行"这条线。

下一篇(二)《多源后台数据接入》:我们把这个最小 Agent 接到真实的数据库、表格、文档与生信文件上,让它能回答跨源分析问题。