2026年6月11日 周四晚上19:30,报名腾讯会议了解“业务抓夹如何成为前线部署工程师(FDE)”(限30人)
免费POC, 零成本试错
FDE知识库

FDE知识库

学习大模型的前沿技术与行业落地应用


我要投稿

从零构建AI Agent:没有魔法,只有循环

发布日期:2026-05-16 08:32:18 浏览次数: 1736
作者:AI拉呱

微信搜一搜,关注“AI拉呱”

推荐语

从零拆解AI Agent的核心循环,让你亲手实现自己的智能体。

核心内容:
1. AI Agent的本质与ReAct循环模式解析
2. 从零构建Agent的完整实验记录与代码实现
3. 本地与云端混合模式的实战应用

杨芳贤
53AI创始人/腾讯云(TVP)最具价值专家
大家好,我是AI拉呱,一个专注人工智领域的知识博主,现任资深算法研究员一职,拥有丰富的AI经验。关注AI拉呱一起学习更多AI知识。

 

从零构建AI Agent:没有魔法,只有循环

作者:AI拉呱(Errol Yan)
定位:AI领域深度内容与实战方法分享

我每天都在用 Claude、Codex、Cursor、Gemini、Copilot 或 Junie,但很长一段时间里,我依然说不清“聊天机器人”到底是从哪一行代码开始,变成了“agent”。

我也讲不明白,究竟是什么让它们具备了 agent 的行为特征。于是我决定自己从零写一个最朴素的版本,把这件事彻底拆开看。

对我来说,理解一个新概念最好的方式,就是亲手做一遍,然后把它讲明白。本文正好做这两件事:它既是一次实验记录,也是一个实操教程。

我们会从大约 50 行 Python 开始,接上 OpenAI,切换到 Ollama 本地模型,再做一个本地 orchestration + 云端 delegation 的混合模式,随后加入 tools、实现 MCP,最后再和 Claude CLI 做一个对照。读完之后,你会清楚看到 agent 在底层到底发生了什么。

没有 LangChain,没有 LangGraph,没有 CrewAI。只有 Python、一个 LLM,以及一个 while 循环。

我们到底要构建什么?

在你动手写之前,先得给它下定义。

一个 AI agent,本质上是一个程序,它需要:

  1. 1. 接收用户给出的高层任务
  2. 2. 判断下一步应该做什么
  3. 3. 采取行动,比如调用工具、搜索网页、读取文件
  4. 4. 观察结果
  5. 5. 决定是继续还是给出最终答案
  6. 6. 保持会话历史,让每一步都建立在前面的结果之上

普通的 LLM 调用是一锤子买卖:给 prompt,拿回答,结束。而 agent 的不同点在于它会循环。它会拿着高层目标不断重复“思考、行动、观察、再决定”的过程,直到任务完成。

这套循环,才是语言模型变成 agent 的关键。

大多数 agent 会遵循一个常见模式,叫 ReAct(Reason + Act)。模型不是直接跳到最终答案,而是先形成一个“想法”,再选择一个“动作”(通常是工具调用),然后根据“观察到的结果”继续推进。

图 1: ReAct 循环。先 Reason,再 Act,观察结果后再推理,直到模型认为已经足够回答。

模型没有真正意义上的意识,也没有人类式反思。它有的,是完整的上下文窗口:它此前做过什么、看到了什么、工具返回了什么,全部保留在消息历史里。ReAct 把这一点组织成了一种“看起来像会自我修正”的行为模式。

每次循环都非常简单:

  1. 1. 把当前会话发送给 LLM,包括 system prompt、用户请求和之前的工具结果
  2. 2. LLM 要么返回最终答案,要么返回一组它想调用的工具
  3. 3. 如果是最终答案,就结束
  4. 4. 如果是工具调用,就执行工具,把结果追加进会话,再回到第 1 步

这就是最核心的 agent 架构。

最小实现:让云端 API 先当“大脑”

第一步,我们用云端 API 当大脑。这里用 OpenAI,是因为它的 tool calling 接口最直观;但只要兼容 OpenAI 风格,Gemini、Anthropic 等都能用类似思路实现。

最核心的 agent 机制其实就是下面这段代码:

def run_agent(task: str, client: OpenAI, model: str = "gpt-4o-mini") -> str:
    messages = [
        {
            "role"
: "system",
            "content"
: (
                "You are a helpful assistant. Use tools when needed. "

                "When you have a final answer, respond without calling any tools."

            ),
        },
        {"role": "user", "content": task},
    ]

    while
 True:
        response = client.chat.completions.create(
            model=model,
            messages=messages,
            tools=TOOLS,
            tool_choice="auto",
        )

        message = response.choices[0].message
        messages.append(message)

        if
 not message.tool_calls:
            return
 message.content

        for
 tool_call in message.tool_calls:
            name = tool_call.function.name
            args = json.loads(tool_call.function.arguments)

            print
(f"  > calling {name}({args})")

            fn = TOOL_FUNCTIONS.get(name)
            result = fn(**args) if fn else f"Unknown tool: {name}"

            messages.append({
                "role"
: "tool",
                "tool_call_id"
: tool_call.id,
                "content"
: result,
            })

关键点就在这句:if not message.tool_calls

如果模型返回的是普通文本,而没有请求调用工具,那就说明它认为自己已经拿到足够信息,可以直接回答。agent 于是退出并返回答案。

如果模型请求了工具,那 agent 就执行这些工具,把结果追加到 messages 里,再回到 while 循环顶部继续下一轮。

这里的 messages,其实就是 agent 的短期记忆。每次工具调用和结果都会被追加进去,因此当模型最终下判断时,它已经看见自己一路做过的所有事。

另外,system prompt 也很重要。它像方向盘一样,规定模型什么时候该用工具、什么时候该停止,以及最终答案应该长什么样。现实中的生产级 agent,system prompt 往往会比这个例子大得多。

定义工具

为了让例子足够具体,我们先加三个最简单的工具:当前日期时间、计算器,以及一个天气 stub。真实项目里,你会把天气 stub 换成真实 API。

import json
import
 os
from
 datetime import datetime
from
 openai import OpenAI


def
 get_current_date() -> str:
    return
 datetime.now().strftime("%Y-%m-%d %H:%M:%S")


def
 calculate(expression: str) -> str:
    try
:
        result = eval(expression, {"__builtins__": {}}, {})
        return
 str(result)
    except
 Exception as e:
        return
 f"Error: {e}"


def
 get_weather(city: str) -> str:
    return
 f"Weather in {city}: 72°F, partly cloudy"


TOOL_FUNCTIONS = {
    "get_current_date"
: get_current_date,
    "calculate"
: calculate,
    "get_weather"
: get_weather,
}

工具 schema 则告诉 LLM:系统里有哪些工具、它们能干什么、需要什么参数。

TOOLS = [
    {
        "type"
: "function",
        "function"
: {
            "name"
: "get_current_date",
            "description"
: "Returns the current date and time",
            "parameters"
: {"type": "object", "properties": {}, "required": []},
        },
    },
    {
        "type"
: "function",
        "function"
: {
            "name"
: "calculate",
            "description"
: "Evaluates a math expression and returns the result",
            "parameters"
: {
                "type"
: "object",
                "properties"
: {
                    "expression"
: {
                        "type"
: "string",
                        "description"
: "A Python math expression, e.g. '2 + 2' or '100 * 0.15'",
                    }
                },
                "required"
: ["expression"],
            },
        },
    },
    {
        "type"
: "function",
        "function"
: {
            "name"
: "get_weather",
            "description"
: "Gets current weather for a city",
            "parameters"
: {
                "type"
: "object",
                "properties"
: {
                    "city"
: {"type": "string", "description": "City name"}
                },
                "required"
: ["city"],
            },
        },
    },
]

运行方式如下:

if __name__ == "__main__":
    client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

    task = "What's today's date? Also, what is 15% of 847? And what's the weather in Tokyo?"
    print
(f"Task: {task}\n")
    answer = run_agent(task, client)
    print
(f"\nAnswer: {answer}")

输出会像这样:

Task: What's today's date? Also, what is 15% of 847? And what's the weather in Tokyo?
  > calling get_current_date({})
  > calling calculate({'expression': '847 * 0.15'})
  > calling get_weather({'city': 'Tokyo'})

Answer: Today is 2026-04-30 09:14:22. 15% of 847 is 127.05.
The weather in Tokyo is 72°F and partly cloudy.

可以看到,模型在第一轮就识别出自己需要哪三个工具,调用完之后再把结果整合成最终答案。

没有框架,没有编排平台,只有一个循环。

把云端 API 换成本地 Ollama

Ollama 提供 OpenAI-compatible API,这意味着上面的 agent 几乎不用改,只改一个 client 配置就能跑本地模型:

ollama_client = OpenAI(
    base_url="http://localhost:11434/v1",
    api_key="ollama",
)

answer = run_agent(task, ollama_client, model="qwen2.5")

就这么简单。对 agent 代码而言,它根本不在乎对面是 OpenAI,还是你本机跑起来的 Ollama。

启动 Ollama 的方式一般是:

ollama pull qwen2.5
ollama serve

之后,这个 agent 就可以完全离线运行。我自己很喜欢用这个模式测试新工具,因为不用烧 API 预算,敏感数据也不会离开本机。

不是所有本地模型都支持 tool calling

这是很多人第一次做本地 agent 时会踩到的坑。

比如我一开始试的是 mistral(Mistral 7B)。程序能正常跑,但输出长这样:

Answer: I need to call get_current_date() to find today's date.
Let me use the calculate tool: calculate(expression="847 * 0.15")...

也就是说,它只是用自然语言“描述”自己想调用工具,却没有真正产生结构化 tool calls。结果就是 response.tool_calls 始终为空,于是 agent 直接退出,把那段描述文本当成最终答案返回。

这不是 agent 代码有 bug,而是模型本身不支持 OpenAI 风格的函数调用格式。

很多模型会模仿出“像是工具调用”的文本,但不会真正输出可执行的结构化 JSON。当前通过 Ollama 对 function calling 支持更稳定的,通常是像 qwen2.5 这类模型。

如果你的 agent 一上来就不调工具,先怀疑模型,再怀疑代码。

构建混合模式:本地编排,云端委派

还有一种非常实用的模式:让本地模型负责 orchestration,只在任务真的复杂时,再调用云端大模型。

也就是说,默认跑本地 agent,但给它一个能“请云端专家出手”的工具:

def ask_cloud_expert(question: str) -> str:
    """Delegate complex questions to a cloud model."""

    cloud_client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
    response = cloud_client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": question}],
    )
    return
 response.choices[0].message.content

把它加入 TOOL_FUNCTIONS 和 TOOLS 之后,本地模型就可以在自己不擅长时,主动把问题交给云端更强模型。

例如:

answer = run_agent(
    task="What's 2+2? Also, explain the philosophical implications of the Ship of Theseus paradox.",
    client=ollama_client,
    model="qwen2.5"
)

这时,本地模型可以自己处理 2+2,但在遇到“忒修斯之船”这种更复杂的哲学问题时,调用 ask_cloud_expert()。你付费的云端调用,就从“整个任务都在云上跑”,变成“只为真正复杂的那一步买单”。

继续加工具:让 Agent 真的开始干活

接下来再加几个更现实一点的工具:web_searchread_file 和 write_file

from pathlib import Path

def
 web_search(query: str) -> str:
    return
 (
        f"Search results for '{query}':\n"

        f"1. Wikipedia: comprehensive overview\n"

        f"2. Recent article: explained in 5 minutes\n"

        f"3. Official docs"

    )

def
 read_file(path: str) -> str:
    return
 Path(path).read_text()

def
 write_file(path: str, content: str) -> str:
    Path(path).write_text(content)
    return
 f"wrote {len(content)} chars to '{path}'"

TOOL_FUNCTIONS = {
    "get_current_date"
: get_current_date,
    "calculate"
: calculate,
    "get_weather"
: get_weather,
    "web_search"
: web_search,
    "read_file"
: read_file,
    "write_file"
: write_file,
}

这些 schema 也加进 TOOLS 后,agent 就不只是会算数和报天气,而是开始具备:

  • • 回答依赖实时信息的问题
  • • 做计算
  • • 获取天气
  • • 搜索网页
  • • 读写文件

到这一步,它已经能做很多实际工作了。

但还有一个明显限制:所有工具都被硬编码在脚本里。也就是说,工具无法复用,别的 agent 也没法直接拿来用。

MCP Client:从外部 Server 发现并使用工具

这正是 MCP(Model Context Protocol)要解决的问题。

MCP 由 Anthropic 在 2024 年底推出,本质上是一个标准协议,定义了 agent 如何从任意 server 发现并调用工具。这个 server 可以是你自己写的,也可以是第三方提供的 GitHub、Slack、Postgres、Google Drive 等服务。

图 2: MCP 架构。一个 client(你的 agent)可以连接多个 server,而每个 server 暴露自己的工具。

当你的 DIY agent 变成 MCP client 后,工具定义就不再需要全部手写在本地脚本里,而是可以通过协议向 server 发现。

从 agent 视角看,MCP 工具和本地函数没什么区别:它们同样会出现在 TOOLS 里,被模型选择,被调用,再返回结果。

真正改变的是,工具从“写死在脚本里”变成“可共享、可复用、可独立维护”。

MCP Server:把你的工具暴露给其他 Agent

反过来,如果你希望自己的工具能被任意 MCP-compatible agent 使用,那就需要写一个 MCP server。

下面是一个极简的例子,它暴露了两个工具:to_uppercase 和 count_words

# mcp_server.py — a real MCP server in 10 lines
from
 mcp.server.fastmcp import FastMCP

mcp = FastMCP("mini-tools")

@mcp.tool()

def
 to_uppercase(text: str) -> str:
    """Convert text to uppercase."""

    return
 text.upper()

@mcp.tool()

def
 count_words(text: str) -> int:
    """Count the number of words in a string."""

    return
 len(text.split())

if
 __name__ == "__main__":
    mcp.run()

它之所以有代表性,不是因为功能复杂,而是因为边界清晰:mcp_server.py 是一个独立进程。agent 通过 JSON-RPC 与它通信,工具调用和结果返回都遵循统一协议。

这意味着,你完全可以把它替换成远端服务,而 agent 代码本身几乎不用改。

任何 MCP-compatible agent 都能接上这个 server:Claude Desktop、Cursor、你自己写的 agent,或者其他支持 MCP 的客户端。

这正是 MCP 生态能扩展起来的原因。过去每个 agent 都要各自重写“调用 GitHub API”或“查询 Postgres”;现在,只要有人把能力封装成 MCP server,其他 agent 就能直接复用。

和 Claude CLI 比起来怎么样?

Claude Code 是生产级工具,而我写的这个 agent,更像学习工具。

它们的差别其实非常明确。

Claude Code 能做很多我这个 agent 做不到的事:

  • • 在任务很大时,自动启子代理并隔离上下文
  • • 在执行危险命令前请求确认
  • • 跨 session 持久化 memory
  • • 工具失败后自动调整参数并重试
  • • 在上下文接近上限时压缩历史消息

而我这个 agent,没有这些“安全护栏”和“成熟基础设施”。它只有几个工具、一个 messages 列表,以及一个循环。工具一旦抛异常,就可能直接崩。

但它的优势也很明确:

  • • 每一行代码我都能看懂
  • • 出问题时,我知道该去哪里查
  • • 它可以完全离线运行
  • • 如果只把复杂步骤委派给云端,我可以把费用压得很低

所以,如果我要交付稳定产品,我会用 Claude Code;但如果我要真正理解 agent 的底层机制,或者做一些我不想被框架束手束脚的实验,那我会从这个循环开始。

框架到底什么时候该上?

你不需要 LangGraph 才能理解 agent 是什么。但当 retries、checkpoints、approval gates 变成刚需时,框架就开始有意义了。

上面这个极简版本没有错误处理。工具抛错就崩,没有重试,没有人工审批,没有跨会话 memory,也没有多 agent 并行。

LangGraph 通过把 agent 建模成显式状态机,解决的是这些“工程化问题”:checkpointing、结构化错误处理、人类介入节点,以及更好的 observability。

CrewAI 和 AutoGen 更偏向多 agent 协作:你可以定义 research、writer、critic 等不同角色,让它们用不同 prompt 或模型协同工作。

Claude Agents SDK 和 OpenAI Assistants API 则是托管式运行时:你把状态管理、工具路由和线程管理交给平台,自己换取更快的交付速度。

那个 50 行版本,更像是一张草图。而 LangGraph 则是在这张草图之上,加上真正能承重的结构。

结论也很简单:

  • • 想上生产,就用框架。
  • • 想真正理解 agent 的底层,就先自己写一遍循环。

这次亲手搭建到底教会了我什么

我最初只是想搞明白,AI agent 到底是怎么工作的。现在我明白了。

自己写这个东西之后,我终于有了完整的心智模型:

  • • 我知道 agent 可能在哪里卡住
  • • 我知道为什么它会选某个工具
  • • 我知道什么时候“工具越多越强”其实是错觉
  • • 我也终于能更清楚理解 Claude Code 或 Cursor 在底层到底做了什么

未来我还是会在很多项目里继续使用 LangGraph、Claude Agents SDK 这类框架,因为它们确实解决了真实问题。但我也会有一些项目,从这个极简版开始,因为我完全知道它在做什么,而且修改它时不需要先和一个庞大抽象层搏斗。

现在你也已经看到了同样的东西:并没有什么神秘魔法。模型只是看着会话历史,判断自己是否有足够信息回答,还是需要调用工具;然后不断重复,直到任务完成。

所谓 retry、human-in-the-loop、memory、multi-agent orchestration,都是在这条循环之上逐渐叠加出来的工程层。

先把最朴素的版本写出来。之后,你再决定自己到底需不需要框架。

53AI,企业落地大模型首选服务商

产品:场景落地咨询+大模型应用平台+行业解决方案

承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

添加专属顾问

回到顶部

加载中...

扫码咨询