Skip to content

从零开发一个 Agent

本页速览 从零开发一个能调用工具完成任务的 Agent:函数定义与 function calling schema、ReAct 工具循环、记忆与规划,用"查天气并写入日程"贯穿全程,附带框架选型、工程要点、评估与常见坑,代码可直接运行。

本页含时效性内容,数据截止于 2025-06;JD、榜单、产品功能等信息可能已变化,引用前请核对原始出处。

从零开发一个 Agent ​

读十篇 Agent 教程,不如亲手跑通一个会"干活"的 Agent。本文用最小可实现路径,从零做一个能调用工具完成任务的小助手——"帮我查天气并写进日程"——每步给出可运行代码,让你看清 Agent 的内核不过是一个循环。

**智能体(Agent)**是一种以大语言模型为决策中枢、能够调用外部工具完成任务、并能感知工具结果继续行动的软件系统。它和"普通调用 LLM"的区别只有一条:模型不只是回答问题,还决定"下一步该做什么"——查资料、执行代码、调 API、写文件,做完一步再看结果决定下一步。

为什么要亲手写?因为"Agent"最容易被营销词包装成玄学,但它的内核极其朴素:一个 while 循环。框架(LangGraph、OpenAI Agents SDK)做的只是把这个循环工程化——加状态管理、加记忆、加并行、加可观测性。你亲手实现一遍循环,之后用任何框架都能秒懂它在干什么。概念全景见Agent 核心概念与Manus 与 Agent 应用。

先看整体地图:

用户请求
   │
   ▼
┌─────────────────────┐
│ LLM 决策层(ReAct)  │ ← 循环:思考 → 决定调用哪个工具
└────────┬────────────┘
         │ 调用工具(function call)
         ▼
┌─────────────────────┐
│  工具层(Tools)     │  get_weather / add_event / ...
└────────┬────────────┘
         │ 把结果喂回 LLM
         ▼
┌─────────────────────┐
│  记忆层(Memory)    │  对话历史 / 向量记忆 / 文件
└────────┬────────────┘
         │ 结果已满足用户请求
         ▼
       最终答案

我们的实现路径分五步:定义工具 → 工具调用循环 → 加记忆 → 加规划 → 完整 Demo。

前置知识

本文假设你已了解:Agent 是什么(Agent 核心概念)、LLM 的基本能力边界(大语言模型)、怎么给模型写指令(提示词工程)。想先看理论上的 ReAct 循环怎么推理,可对照Transformer 与注意力机制理解"上下文"如何承载整个循环的记忆。

一、定义工具:function calling schema ​

要让 LLM"会用"工具,需要给它一份机器可读的工具说明书(schema),说明每个工具叫什么、干什么、参数有哪些。LLM 不会执行任何代码,它只是输出一段结构化的"工具调用请求",由你的程序解析后执行。

① 工具实现(真正的逻辑)

python
import json
from pathlib import Path

def get_weather(city: str, date: str = "today") -> str:
    """查询某个城市的天气。演示用模拟数据,真实项目替换为天气 API。"""
    import random
    conditions = ["晴", "多云", "小雨", "大风"]
    return json.dumps({
        "city": city, "date": date,
        "weather": random.choice(conditions),
        "temp": f"{random.randint(18, 34)}℃",
    }, ensure_ascii=False)

def add_event(date: str, time: str, title: str) -> str:
    """往日历写入一条日程。演示用本地 JSON 文件,真实项目接日历 API。"""
    path = Path("events.json")
    events = json.loads(path.read_text(encoding="utf-8")) if path.exists() else []
    events.append({"date": date, "time": time, "title": title})
    path.write_text(json.dumps(events, ensure_ascii=False, indent=2), encoding="utf-8")
    return f"OK,已把「{title}」写入 {date} {time}"

② function calling schema(OpenAI 风格,各家大同小异)

python
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询某个城市某天的天气,返回天气现象和气温。",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名,例如:北京、上海"},
                    "date": {"type": "string", "description": "日期(YYYY-MM-DD),缺省为今天"},
                },
                "required": ["city"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "add_event",
            "description": "把一条日程写入日历。",
            "parameters": {
                "type": "object",
                "properties": {
                    "date": {"type": "string", "description": "日程日期(YYYY-MM-DD)"},
                    "time": {"type": "string", "description": "日程时间(HH:MM)"},
                    "title": {"type": "string", "description": "日程标题"},
                },
                "required": ["date", "time", "title"],
            },
        },
    },
]

工具描述决定成败

schema 里最关键的不是 name 而是 description 和参数 description——LLM 靠描述决定"什么时候该用、参数怎么填"。描述含糊(如"处理天气")会导致模型该用时不用、参数乱填;描述要写清触发条件、参数含义、取值范围。这本质上是提示词工程,方法论见提示词工程。

工具实现与 schema 之间的派发器(dispatcher)——把模型请求的名字和 JSON 参数映射到真实函数:

python
def dispatch(name: str, args_json: str) -> str:
    """把 LLM 的工具调用请求派发到真实实现。"""
    args = json.loads(args_json)
    if name == "get_weather":
        return get_weather(args.get("city"), args.get("date", "today"))
    if name == "add_event":
        return add_event(args["date"], args["time"], args["title"])
    return f"错误:未知工具 {name}"

二、LLM 工具调用循环:ReAct ​

工具定义好后,核心就是那个循环。经典范式叫 ReAct(Reasoning + Acting):模型先"推理"(reason)下一步该做什么,再"行动"(act)——输出工具调用请求;你的程序执行工具,把结果作为"观察"(observation)喂回;模型基于观察继续推理。如此循环,直到模型认为任务完成、直接输出答案。这个概念与提示词工程的关系详见提示词工程。

循环结构(伪代码):
while 步数 < max_steps:
    reply = LLM(messages, tools)          # 1. 让模型决定下一步
    if reply 没有工具调用: return reply   # 2a. 完成 → 输出最终答案
    for 每个工具调用:
        result = dispatch(工具名, 参数)    # 2b. 执行工具
        messages.append(tool 消息)         # 3. 把观察结果喂回对话

完整实现(OpenAI 新版接口):

python
from openai import OpenAI

client = OpenAI()   # 需要环境变量 OPENAI_API_KEY;本地 Ollama 见文末

def run_agent(user_query: str, max_steps: int = 6, model: str = "gpt-4o-mini") -> str:
    messages = [{"role": "user", "content": user_query}]
    for step in range(max_steps):
        resp = client.chat.completions.create(
            model=model,
            messages=messages,
            tools=TOOLS,          # 把工具 schema 给模型
        )
        msg = resp.choices[0].message
        messages.append(msg)      # 模型的回复(含工具调用请求)进入对话

        if not msg.tool_calls:    # 模型决定直接回答 → 任务完成
            return msg.content

        # 逐个执行工具调用,结果以 role="tool" 的消息喂回
        for call in msg.tool_calls:
            result = dispatch(call.function.name, call.function.arguments)
            print(f"[step {step}] 调用 {call.function.name}({call.function.arguments}) -> {result}")
            messages.append({
                "role": "tool",
                "tool_call_id": call.id,
                "content": result,
            })
    return "已达到最大步数限制,任务未完成。"

跑一次:

python
print(run_agent("北京今天适合跑步吗?查一下天气再给建议。"))

典型输出轨迹(print 的那几行)会是这样:

[step 0] 调用 get_weather({"city":"北京","date":"today"}) -> {"city":"北京","date":"today","weather":"晴","temp":"27℃"}
北京今天晴、27℃,非常适合跑步。建议傍晚出门,注意补水。

三个容易忽略的细节

  1. 消息历史不能丢:模型回复 msg 必须原样 append 进 messages(包括其中的 tool_calls 字段),否则工具结果和调用对不上,模型会"失忆"。
  2. 一个回复可能调用多个工具:msg.tool_calls 是列表,要逐个执行、逐个回填。
  3. tool_call_id 必须回传:工具结果消息里的 tool_call_id 与调用一一对应,这是多工具场景不出错的关键。

本地 Ollama 版:只需换 client 与模型名,其余代码不变,实现完全离线的 Agent:

python
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
model = "qwen2.5:7b"     # 需支持 function calling;先 ollama pull qwen2.5

三、加记忆 ​

循环里所有信息都在 messages 里流动,这本身就是会话内记忆——多轮对话、上一步的工具结果都靠它。但有两类记忆需要显式设计:

记忆类型存什么实现何时有用
会话内记忆本次任务的全部消息就是 messages 列表多步任务、多轮对话
长期记忆用户偏好、事实、历史结论文件 / 数据库 / 向量库跨会话复用、个性化
工作记忆中间计算结果变量、临时文件复杂计算任务
向量记忆"语义可检索"的历史信息向量库 + 相似度检索历史太多时按需召回

会话内记忆无需额外代码(上文循环天然支持)。向量记忆的典型实现:任务中产生的关键结论写入向量库,之后按需检索,避免把全部历史塞进上下文(上下文越长越贵,见推理优化与量化)。原理与实现见向量数据库与语义检索:

python
import numpy as np
import faiss
from sentence_transformers import SentenceTransformer

mem_model = SentenceTransformer("BAAI/bge-small-zh-v1.5")
mem_index = faiss.IndexFlatIP(384)   # 语义记忆库(进程内)
memory_texts: list[str] = []

def remember(text: str) -> None:
    v = mem_model.encode([text], normalize_embeddings=True)
    mem_index.add(v.astype("float32"))
    memory_texts.append(text)

def recall(query: str, k: int = 3) -> list[str]:
    if not memory_texts:
        return []
    qv = mem_model.encode([query], normalize_embeddings=True)
    _, ids = mem_index.search(qv.astype("float32"), k)
    return [memory_texts[i] for i in ids[0] if i >= 0]

# 例子:把"用户每周二晨跑"写进记忆,下次自动提到
remember("用户的习惯:每周二早上 7 点晨跑")
print(recall("这个用户有什么运动习惯?"))   # ['用户的习惯:每周二早上 7 点晨跑']

记忆的容量管理

所有记忆最终都要变成 token 进上下文。会话长了要截断或压缩(保留最近 N 轮 + 摘要);向量记忆召回 k 要小。没有容量管理的 Agent,会在第 20 轮对话后把上下文撑爆、又贵又慢。

四、加规划:任务分解 ​

复杂任务("帮我安排周末")一次工具调用往往做不完。加一层规划器:先让 LLM 把大任务拆成有序子任务,再逐个执行、合并结果。这对应 Agent 的 planning 能力:

python
def plan(task: str) -> list[str]:
    """把任务拆成 2~4 个有序子任务,每行一个。"""
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system",
             "content": "把用户任务拆成 2~4 个有序子任务,每行一个,只输出子任务本身。"},
            {"role": "user", "content": task},
        ],
    )
    return [ln.strip("- ").strip() for ln in resp.choices[0].message.content.splitlines() if ln.strip()]

然后让执行循环带规划跑:先 plan,再对每个子任务调用 run_agent,最后汇总:

python
def run_with_plan(task: str) -> str:
    steps = plan(task)
    print("规划:", steps)
    results = [run_agent(f"子任务:{s}") for s in steps]
    # 汇总子任务结果,交给 LLM 生成最终答复
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "system", "content": "根据下面的子任务执行结果,给用户一个完整、友好的最终答复。"},
                  {"role": "user", "content": "\n\n".join(f"### {s}\n{r}" for s, r in zip(steps, results))}],
    )
    return resp.choices[0].message.content

规划的两层皮

规划有两种做法:显式规划(上面的先拆后做,可控、可审计)与隐式规划(ReAct 循环里模型自己边做边想,灵活但难预测)。入门阶段先做显式:拆步、执行、汇总三步走,出了问题能定位到具体子任务。复杂任务的规划边界与"任务分解"的深入讨论见Agent 核心概念。

五、完整可运行 Demo:查天气并写进日程 ​

把以上全部拼成 agent_demo.py。目标任务:"帮我查一下北京和上海明天的天气,把天气更好的那个城市安排一次明天下午 3 点的户外跑步。"

python
"""
agent_demo.py —— 最小可运行的 Agent:查天气 + 写日程

用法:python agent_demo.py
依赖:pip install openai  (Ollama 本地版:见代码末尾注释)
"""
import json
import random
from pathlib import Path

from openai import OpenAI

# ── 工具实现 ─────────────────────────────────────────────
def get_weather(city: str, date: str = "today") -> str:
    conditions = ["晴", "多云", "小雨", "大风"]
    return json.dumps({"city": city, "date": date,
                       "weather": random.choice(conditions),
                       "temp": f"{random.randint(18, 34)}℃"},
                      ensure_ascii=False)

def add_event(date: str, time: str, title: str) -> str:
    path = Path("events.json")
    events = json.loads(path.read_text(encoding="utf-8")) if path.exists() else []
    events.append({"date": date, "time": time, "title": title})
    path.write_text(json.dumps(events, ensure_ascii=False, indent=2), encoding="utf-8")
    return f"OK,已把「{title}」写入 {date} {time}"

# ── function calling schema ─────────────────────────────
TOOLS = [
    {"type": "function", "function": {
        "name": "get_weather",
        "description": "查询某个城市某天的天气,返回天气现象和气温。",
        "parameters": {"type": "object",
                       "properties": {"city": {"type": "string", "description": "城市名"},
                                      "date": {"type": "string", "description": "日期,缺省 today"}},
                       "required": ["city"]}}},
    {"type": "function", "function": {
        "name": "add_event",
        "description": "把一条日程写入日历。",
        "parameters": {"type": "object",
                       "properties": {"date": {"type": "string", "description": "日期 YYYY-MM-DD"},
                                      "time": {"type": "string", "description": "时间 HH:MM"},
                                      "title": {"type": "string", "description": "标题"}},
                       "required": ["date", "time", "title"]}}},
]

def dispatch(name: str, args_json: str) -> str:
    args = json.loads(args_json)
    if name == "get_weather":
        return get_weather(args.get("city"), args.get("date", "today"))
    if name == "add_event":
        return add_event(args["date"], args["time"], args["title"])
    return f"错误:未知工具 {name}"

# ── ReAct 循环 ───────────────────────────────────────────
client = OpenAI()                       # 需 OPENAI_API_KEY
# 本地版:client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
#         model = "qwen2.5:7b"

def run_agent(user_query: str, max_steps: int = 8, model: str = "gpt-4o-mini") -> str:
    messages = [{"role": "user", "content": user_query}]
    for step in range(max_steps):
        resp = client.chat.completions.create(model=model, messages=messages, tools=TOOLS)
        msg = resp.choices[0].message
        messages.append(msg)
        if not msg.tool_calls:
            return msg.content
        for call in msg.tool_calls:
            result = dispatch(call.function.name, call.function.arguments)
            print(f"[step {step}] {call.function.name}({call.function.arguments}) -> {result}")
            messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
    return "已达到最大步数限制,任务未完成。"

if __name__ == "__main__":
    q = "帮我查一下北京和上海明天的天气,把天气更好的那个城市安排一次明天下午 3 点的户外跑步。"
    print(run_agent(q))
    print("—— 日程文件 events.json 内容 ——")
    print(Path("events.json").read_text(encoding="utf-8"))

运行后你会看到模型自主完成"查两个城市 → 比较 → 调 add_event"的完整链条,并给出带依据的最终答复。这个 80 行脚本就是 Agent 的完整骨架——所有框架做的事情,都在这个循环之上加工程化。

六、框架选择:什么时候别自研 ​

自己写的循环适合学习和极简场景;真实项目里,工具数量、状态复杂度、并发、可观测性会迅速超出手写循环的舒适区。常见框架对比:

框架核心抽象优点代价何时用
LangGraph图 + 状态机流程可控、支持分支/循环/并行、可检查点概念多、上手曲线陡复杂多步流程、需要精确控制与断点续跑
OpenAI Agents SDKAgent + Handoff(交接)官方维护、函数调用开箱即用、代码量少绑定 OpenAI 生态快速交付、单 Agent 为主
Claude Agent SDKAgent + 工具 + computer useAnthropic 官方、工具/沙箱体验好绑定 Claude 生态深度用 Claude、需要浏览器/电脑操作
自研(本文)while 循环零依赖、完全透明、可控缺工程能力(状态/重试/观测)学习、工具 <5 个、流程固定

选择判断一句话:工具少于 5 个、流程固定 → 自研够用;要做产品、要并行与审计 → 上框架。无论用哪个框架,"工具 schema + 循环 + 记忆"的心智模型是通用的,框架只是换个实现。模型选型与成本参考见模型与榜单速查。

七、工程要点:从 demo 到能上线 ​

手写循环跑通后,下面这些问题会在真实场景立刻冒出来:

1. 工具返回截断 ​

工具可能返回超长文本(查数据库、读文件)。先把工具返回截断再喂回,否则上下文被撑爆、token 成本飙升:

python
def safe_tool_result(result: str, max_len: int = 4000) -> str:
    if len(result) > max_len:
        return result[:max_len] + f"\n……(已截断,原文 {len(result)} 字符)"
    return result

2. 错误重试与容错 ​

工具可能抛异常(API 超时、参数非法)。把异常转成文本喂回模型,让它自己修正,而不是让整个循环崩溃:

python
try:
    result = dispatch(call.function.name, call.function.arguments)
except Exception as e:          # 用 isinstance 细分异常类型更严谨
    result = f"工具执行失败:{type(e).__name__}: {e}"
messages.append({"role": "tool", "tool_call_id": call.id, "content": safe_tool_result(result)})

3. 最大步数与死循环检测 ​

max_steps 必须有,且要检测重复动作(同一工具 + 同一参数连续 N 次),直接判定失败。死循环是 Agent 最常见的"事故"形态。

4. 成本控制 ​

每一步循环都是一次 LLM 调用。控制手段:用便宜的小模型做规划/简单工具调用、加语义缓存(相似问题命中缓存)、压缩历史消息。这些优化见推理优化与量化和部署与推理优化实战。

5. 权限与安全 ​

工具是 Agent 对外部世界施加影响的唯一通道,也是最危险的地方:

风险场景对策
提示注入工具返回的内容(如网页抓取结果)里藏指令,诱导 Agent 执行恶意操作工具结果按"数据"而非"指令"处理;对敏感操作做二次确认;上下文隔离
越权工具调用模型调用了一个不该调的工具(如删除操作)最小权限原则:每个工具单独授权、敏感操作需用户确认
沙箱执行让 Agent 执行生成代码在沙箱/容器内运行,禁止访问敏感路径
参数校验工具参数来自模型,可能非法dispatch 层做类型与取值范围校验

安全边界与治理框架详见AI 安全与治理。

两条红线

  1. 永远不要把"删除""转账"这类不可逆工具直接交给 Agent 自动调用,必须加人工确认环节。
  2. 工具输出里混入的外部文本(网页、邮件、文档)默认不可信——它们可能携带攻击性指令。提示注入是对 Agent 最现实的攻击面。

八、评估 Agent 与常见坑 ​

评估 ​

Agent 是"过程 + 结果"都要评。最实用的四个指标:

指标度量什么怎么测
任务成功率端到端有没有完成任务一批真实任务,人工判定成功/失败
工具调用正确率该调的工具调对没、参数对不对对比"预期工具调用序列"与实际的差异
步数效率用了多少步完成统计平均步数;异常值往往对应"绕弯"
成本每次任务多少 token记录 tokens 用量,关联任务类型

建议:先建 20~50 条任务的评估集,再改循环逻辑,每次改动跑一遍,别靠"感觉变好了"。评估管道的搭建见搭建一套 LLM 评估,指标理论见LLM 评估与基准。

常见坑 ​

坑症状根因对策
循环失控无限调用工具、token 烧穿没有步数上限、模型反复试错max_steps + 重复动作检测 + 超时
幻觉工具参数调用了不存在的城市/参数类型错schema 描述不清、模型自由发挥加强参数描述、dispatch 层校验、必填参数缺失时重试
上下文膨胀越到后面越慢越贵、答非所问历史消息全量堆叠截断/压缩/向量记忆按需召回
工具结果"污染"行为突然异常工具返回里混入指令性文本结果截断、标注为数据、隔离上下文
过度调用工具一个简单的"你好"也调工具schema 触发条件写得太宽description 写清"何时不用调"
静默失败任务"看起来完成"实则没执行工具返回了错误但被当作成功工具返回结构化状态(ok/error),循环里显式检查

更完整的 Agent 反模式清单见常见陷阱与反模式。

九、进阶:多 Agent 协作 ​

单 Agent 有容量和职责边界,复杂任务让多个各司其职的 Agent 协作。三种主流编排模式:

模式结构适用例子
编排者-执行者(orchestrator-worker)一个主 Agent 拆任务、分派给专业子 Agent任务可分解、子任务类型固定写报告 = 检索 Agent + 写作 Agent + 排版 Agent
流水线(pipeline)前一个 Agent 的输出是后一个的输入任务天然有先后顺序调研 → 分析 → 总结
辩论/评审(debate)多个 Agent 各自给答案再互相评审高正确性要求代码评审、事实核查

工程上的配套:子 Agent 之间只通过"消息/结果"交互(不要共享可变状态)、给每个子 Agent 独立上下文隔离、主 Agent 负责汇总与冲突裁决。多 Agent 产品的完整解剖见Manus 与 Agent 应用;决定协作结构的依据,其实回到了Agent 核心概念里对"任务复杂度与 Agent 形态"的讨论。

收尾判断

一套"合格"的 Agent:能稳定完成任务、有步数与成本护栏、工具不可逆操作有人工确认、并且有一份评估集量化"没变坏"。做到这四点,它已经是一个可以谈工程化的系统,而不是一个 demo。术语不懂查术语表,全景看什么是 AI 热门概念。

十、延伸阅读 ​

参考资料 ​