本文通过一个简单的50行Python代码示例,详细介绍了如何将语言模型(LLM)从基础的“聊天机器人”升级为具备推理、行动和决策能力的“智能体”。作者从零开始构建了一个基础的Agent,通过接入OpenAI和Ollama本地模型,展示了Agent如何通过“思考-行动-观察-决定”的ReAct循环来完成任务。文章还介绍了如何定义工具、使用MCP(模型上下文协议)从外部服务器发现工具,并与Claude CLI进行了对比。最后,作者强调了理解Agent底层工作原理的重要性,并建议在需要复杂功能时使用LangGraph等框架。
我每天都在用 Claude、Codex、Cursor、Gemini、Copilot 或 Junie,但我依然说不清到底是哪一行代码让“chatbot”变成了“agent”,也解释不了它们为何算是 agent。于是我从零写了一个最天真的版本,自己找答案。
对我来说,理解一个新概念的最好方式,就是把它做出来,再讲给别人听。本文两者兼顾。我把实验故事和实操教程结合在一起,我敢保证你会觉得有用。
我们将从区区 50 行 Python 开始,先接入 OpenAI,再通过 Ollama 切换到本地模型,构建本地+云端混合模式,加入 tools,接入 MCP,最后和 Claude CLI 做个对比。看完你就能清清楚楚地看到“底层到底发生了什么”。
不需要 LangChain。也不需要 LangGraph、CrewAI。只用 Python、一个 LLM,再加一个 while 循环。
我们要构建什么(规格说明)
在动手之前,先定义清楚它是什么、做什么(spec)。
一个 AI agent(下文统一用 Agent 指代“智能体”)是一个程序,它会:
- 接受用户给出的高层任务
- 推理“下一步该做什么”
- 采取行动(调用 tool、搜索网页、读取文件)
- 观察结果
- 决定是继续迭代还是返回最终答案
- 维护对话历史,让每次决策都建立在先前步骤之上
普通的 LLM 调用是一次性(one-shot):你发出 prompt,它给你回复,结束。Agent 不同在于它会循环。它接收高层任务,推理下一步,调用 action,观察结果,如此反复,直到任务完成。
这个“思考-行动-观察-决定”的循环,才是把语言模型变成 agent 的关键。
多数 Agent 采用一种叫 ReAct(Reason + Act)的模式。LLM 不会直接跳到最终答案,它会先产出“thought(想法/计划)”,再产出“action(工具调用)”,然后等待“observe(观察)”结果,再决定下一步做什么。
Figure 1: ReAct 循环——先 Reason 再 Act,观察结果,再次推理。循环往复,直到模型有足够信息可以直接作答。
模型没有“意识”,也没有任何有意义的“自我反省”。它拥有的只是对话历史:它做过的每个动作、得到的每个结果,都在 context window(上下文窗口)里。ReAct 模式把这些变成类似“自我反省/自我纠错”的行为,而且确实有效。
每一轮循环会发生这些事:
- 你把当前对话发给 LLM:system prompt、用户消息、以及之前的 tool 结果
- LLM 返回要么是最终答案,要么是一组它想调用的 tools
- 如果是最终答案,就结束
- 如果是 tool 调用,就执行它们,把结果追加到对话历史,再回到第 1 步
这就是全部架构。
最小实现(云端 API 作为大脑)
先用云端 API 当大脑。我选了 OpenAI,因为它的 tool calling 接口最干净,但任何 OpenAI-compatible API 都可以用。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) # This is the decision point: does the model have an answer, or does it need tools? if not message.tool_calls: return message.content # If we reach here, the model called one or more tools 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, }) # End of iteration - go back to the top of the while loop关键行是if not message.tool_calls。如果模型返回的是文本、且没有请求任何 tool,就表示它已经具备回答所需的一切。Agent 退出并返回该文本。如果模型请求了 tool,Agent 就执行它们,把结果追加到对话历史,然后把所有内容再发回模型进入下一轮。
messages列表就是 Agent 的短期记忆。每次 tool 调用和每个结果都会被追加进去。等 LLM 判断“完成”时,它已经“看过”自己做过的所有事和学到的所有信息。
system prompt 同样很重要。它是方向盘,告诉模型何时用 tool、何时停止、最终答案应该是什么样子。在真实的生产 Agent 中,这个 system prompt 往往相当庞大,这点从 Anthropic、Apple 等偶尔的泄露中也能看见。
定义 Tools
先来三个简单的,便于具体化:当前日期/时间、计算器和天气(stub)。在真实 Agent 里,你会把 stub 换成实际 API 调用。
import jsonimport osfrom datetime import datetimefrom openai import OpenAIdef 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: # Replace with a real weather API call 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 有哪些可用的东西。这段 JSON 就是模型在决定调用哪个 tool、用什么参数时所“看到”的内容:
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.在第一轮,LLM 识别到它需要三个 tool,逐个调用、拿到结果,然后组装出最终答案。没有框架、没有编排层。
用 Ollama 将云端 API 换成本地 LLM
Ollama 暴露的是 OpenAI-compatible API,所以同样的 Agent 代码只需要改一处就能跑在本地模型上:
ollama_client = OpenAI( base_url="http://localhost:11434/v1", api_key="ollama", # required by the client library, ignored by Ollama)answer = run_agent(task, ollama_client, model="qwen2.5")就这样。代码根本不知道它是在和 OpenAI 的服务器对话,还是在和你机器上的模型对话。
让 Ollama 跑起来:
# install from ollama.com, then:ollama pull qwen2.5ollama serve之后,Agent 就完全离线运行。我用这种方式测试新 tool,既不烧 API 点数,也能在敏感数据不出本机的场景下使用。
不是所有本地模型都支持 tool calling
这一点会坑到你。我一开始试了mistral(Mistral 7B),它经常被推荐为很能打的本地模型。Agent 运行没报错,但输出类似:
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,但没有真正发出 tool 调用。response.tool_calls每次都是空的,于是它立刻带着模型写下的文字就退出了。
这不是 Agent 代码的 bug。代码严格按写的逻辑工作:检查有无 tool 调用,没找到就返回。问题在于 Mistral 7B 并不支持 OpenAI 风格的结构化 function calling。它被训练成用散文“描述”动作,而不是产出结构化 JSON。模型“幻写”了它以为我想要的语法。
通过 Ollama、能稳定支持 function calling 的模型有:
Table 1
如果你的 Agent 立即返回、从不调用任何 tool,先怀疑模型,不要怀疑代码。换成qwen2.5试试,看看行为有没有变化。
构建混合模式(本地编排,云端委托)
你可以在本地做编排,只有在任务真的需要时才为云端调用买单。默认用本地模型跑 Agent 和简单工具,但提供一个 tool,把复杂推理问题丢给云端模型。本地模型负责循环和简单工具;遇到需要更深推理的内容,就委托。
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,并把对应 schema 加进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(通过 calculator tool),并意识到哲学问题超出它的深度,于是调用ask_cloud_expert(),从 GPT-4 拿到一个像样的答案。你只为一次云端 API 调用买单,而不是为数十次买单。
增加更多 Tools
让我们扩展出更贴近真实世界的工具:web_search、read_file和write_file。
from pathlib import Pathdef web_search(query: str) -> str: # Stub - replace with Brave Search API, SerpAPI, or Tavily 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: # Safe path validation omitted for brevity 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 现在就能搜索网页、并把结果持久化到磁盘。上述web_search是 stub,文件操作也做了简化。完整项目在 github.com/sergenes/mini_agent 中包含了更严谨的路径校验与错误处理。
有了这 6 个 tool,Agent 现在可以:
- 回答需要当前信息的问题(日期/时间)
- 进行计算
- 查询天气
- 搜索网页
- 读写文件
这已经足够做实事了。剩下的问题是:每个 tool 都是硬编码在脚本里的。没法和别的 Agent 共享,也没法直接使用别人做好的工具。
MCP 客户端:从外部服务器发现工具
上面的 Agent 缺少一件事:跨项目共享/复用工具的能力。一切都硬编码在脚本里。想让另一个 Agent 用同样的工具?复制粘贴。想用别人的工具?改写。
MCP(Model Context Protocol,模型上下文协议)由 Anthropic 于 2024 年 11 月推出,它就是为此而生的标准。它定义了一种统一方式,让任何 Agent 都能从任何服务器发现并调用工具:无论是你自己的,还是第三方的 GitHub、Slack、Postgres、Google Drive 等等,成百上千的工具。
Figure 2: MCP 架构——一个客户端(你的 Agent),多个服务器。每个服务器暴露自己的工具。Agent 以同样的方式发现和调用它们,而不关心服务器背后是什么。
你的 DIY Agent 变成一个 MCP 客户端。你不再硬编码 tool 定义,而是去调用服务器,拿回它暴露出来的工具:已经被发现、已经带描述,可以直接传给你的 LLM。
Agent 的逻辑不变,变化的是工具的来源以及谁来维护它们。
配套项目里包含mcp_client.py,它会把 MCP 服务器以子进程的方式拉起来,并通过 JSON-RPC 调用工具。从 Agent 的视角看,MCP 工具和本地定义的工具没有任何区别:同样出现在TOOLS,同样被调用、同样返回结果。
关键洞见是:Agent 并不在乎一个 tool 是不是同一文件里的 Python 函数,还是在互联网上另一端的服务。只要它说 MCP 这门“语言”,就能用。
MCP 服务器:把你的工具暴露给任意 Agent
反过来:如果你想把自己的工具暴露给所有 MCP-compatible 的 Agent 使用,你就写一个 MCP server。
下面是一个完整的 MCP 服务器,暴露了两个工具——to_uppercase和count_words:
# mcp_server.py - a real MCP server in 10 linesfrom mcp.server.fastmcp import FastMCPmcp = 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 调用一个 tool,子进程启动、完成 JSON-RPC 握手、结果回传。你完全可以把它换成跑在互联网上另一头的服务器,而 Agent 代码一行都不用改。
现在,任何 MCP-compatible 的 Agent 都可以用你的工具——Claude Desktop、Cursor、你的 DIY Agent,谁都行。你发布 server,大家在配置里指过去,就能直接用。
生态就是这样扩展的。不是每个 Agent 都去重复实现“调用 GitHub API”或“查询 Postgres”,而是某个人写一个 MCP server,大家共用。
与 Claude CLI 的对比
Claude Code 是生产力工具。我的 Agent 是学习工具。这是诚实的对比,也值得弄明白为什么。
Claude Code 做了我的 Agent 做不到的事:当任务很大时,会启动带隔离上下文窗口的子 Agent;在执行破坏性命令前会请求确认;在会话间保持持久记忆;在工具调用失败时会调整参数重试;在逼近上下文上限时会压缩历史消息。我的 Agent 全都没有。它只有 6 个工具、一个messages列表,没有安全网。工具抛异常,它就崩。
我的 Agent 的优势是:我能读懂它的每一行。出问题时,我知道去哪一行看。我可以让它完全离线地用 Ollama 跑,或者接混合模式,只为确实需要的云端调用付费。Claude Code 是按消息计费。而我的 Agent,除非我让它调用 GPT-4,否则不花钱。
如果我要交付可靠的东西,我会用 Claude Code。如果我要搞清楚“底层到底在发生什么”,或者要原型实现一些用框架会“打架”的东西,我就从这个循环开始。
该轮到框架出场的时刻
你不需要 LangGraph 才能弄懂什么是 Agent。你需要它,是当重试、检查点、审批闸门变得不可或缺的时候。
上面的代码没有错误处理。工具抛异常,Agent 就崩。没有重试逻辑。没有在风险操作前暂停等待人工批准。除了单条对话,没有其它记忆能力。也不会为并行工作生成子 Agent。
LangGraph 用“状态机(state machine)”的方式解决这些问题:用显式的节点与边来建模 Agent。你定义每一步做什么、什么条件触发下一步。代价是前期搭建更多,但你能得到 checkpointing、结构化错误处理、human-in-the-loop 步骤,以及对 Agent 在干什么、为什么这么干的全量可观测性。
CrewAI 和 AutoGen 专注多 Agent 协作。不是一个 Agent 用很多工具,而是定义多个角色(researcher、writer、critic),编排它们如何沟通。适合复杂任务,需要不同步骤用不同 prompt 或不同模型的场景。
Claude Agents SDK 和 OpenAI Assistants API 则是托管运行时,你把状态管理、tool 路由、线程等交给平台。控制力少一些,但交付更快。
50 行版本是一张草图。LangGraph 则是把这张草图变成有“承重墙”的建筑。
要做生产:用框架。要搞懂本质:自己写这个循环。
我从这个项目里学到了什么
我想弄懂 AI Agent 是如何工作的。现在我懂了。
搭起来之后,我补齐了之前缺失的心智模型。我能看见 Agent 会卡在哪里、为什么会偏好某个 tool、以及什么时候“加更多工具”反而会变糟。我知道当 Claude Code 启动子 Agent 时发生了什么,或者 Cursor 决定重试一次失败操作时发生了什么。
我有一些项目需要 agentic behavior(Agent 式行为)。有些会用 LangGraph 或 Claude Agents SDK——这些框架确实解决了我不想重复造轮子的真实问题。但也有些会从这 50 行版本起步,因为我确切知道它做了什么,而且我可以在不与陌生抽象“搏斗”的前提下改它。
你现在也看到了:没有魔法。模型观察对话历史,判断自己是否“够了可以直接回答”,或者“还需要一个 tool”,然后不断重复,直到完成。其它的一切——重试逻辑、human-in-the-loop、记忆、多 Agent 编排——都是在这个循环之上搭起来的。
当你去用一个框架时,你会知道它在替你做什么。当你不需要它时,你也不会引入一个你调不通的依赖。
先把天真的版本造出来。再做决定。
AI行业迎来前所未有的爆发式增长:从DeepSeek百万年薪招聘AI研究员,到百度、阿里、腾讯等大厂疯狂布局AI Agent,再到国家政策大力扶持数字经济和AI人才培养,所有信号都在告诉我们:AI的黄金十年,真的来了!
在行业火爆之下,AI人才争夺战也日趋白热化,其就业前景一片蓝海!
我给大家准备了一份全套的《AI大模型零基础入门+进阶学习资源包》,包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。😝有需要的小伙伴,可以VX扫描下方二维码免费领取🆓
人才缺口巨大
人力资源社会保障部有关报告显示,据测算,当前,****我国人工智能人才缺口超过500万,****供求比例达1∶10。脉脉最新数据也显示:AI新发岗位量较去年初暴增29倍,超1000家AI企业释放7.2万+岗位……
单拿今年的秋招来说,各互联网大厂释放出来的招聘信息中,我们就能感受到AI浪潮,比如百度90%的技术岗都与AI相关!
就业薪资超高
在旺盛的市场需求下,AI岗位不仅招聘量大,薪资待遇更是“一骑绝尘”。企业为抢AI核心人才,薪资给的非常慷慨,过去一年,懂AI的人才普遍涨薪40%+!
脉脉高聘发布的《2025年度人才迁徙报告》显示,在2025年1月-10月的高薪岗位Top20排行中,AI相关岗位占了绝大多数,并且平均薪资月薪都超过6w!
在去年的秋招中,小红书给算法相关岗位的薪资为50k起,字节开出228万元的超高年薪,据《2025年秋季校园招聘白皮书》,AI算法类平均年薪达36.9万,遥遥领先其他行业!
总结来说,当前人工智能岗位需求多,薪资高,前景好。在职场里,选对赛道就能赢在起跑线。抓住AI风口,轻松实现高薪就业!
但现实却是,仍有很多同学不知道如何抓住AI机遇,会遇到很多就业难题,比如:
❌ 技术过时:只会CRUD的开发者,在AI浪潮中沦为“职场裸奔者”;
❌ 薪资停滞:初级岗位内卷到白菜价,传统开发3年经验薪资涨幅不足15%;
❌ 转型无门:想学AI却找不到系统路径,83%自学党中途放弃。
他们的就业难题解决问题的关键在于:不仅要选对赛道,更要跟对老师!
我给大家准备了一份全套的《AI大模型零基础入门+进阶学习资源包》,包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。😝有需要的小伙伴,可以VX扫描下方二维码免费领取🆓