☰
100行Python复刻OpenClaw:实现AI Agent最小骨架核心链路
2026/10/22 6:06:49 网站建设 项目流程

做AI Agent也有一段时间了,前后翻过不少开源项目,也自己从零搭过几套所谓“智能体应用”。最近总有人私信问我一个问题:如果不依赖那些重型的Agent框架,能不能用最小代价,写出一个真正“能干活”的Agent骨架?我的回答是,能。这篇文章就拿最近社区里讨论度比较高的OpenClaw这类“个人终端AI助手框架”当靶子,试着用大约100行Python代码,复刻出它的核心链路:让模型自己决定调用哪些工具、怎么处理工具返回结果、怎么在多次循环之后收敛到一个最终答案。文章适合刚入门Agent、被各种概念绕晕的人,也适合已经写了几个Demo但总觉得不踏实的同学——你会在这篇文章里看到一套可以亲手敲进去、能运行、能扩展的最小实现,以及它背后真正值得理解的几个设计决策。

1. 先想清楚:AI Agent到底在干什么

很多初学者对“AI Agent”的第一印象,是把它当成一个“更聪明的聊天机器人”。启动一个网页对话框,输入文字,得到回答,仅此而已。但如果你用过OpenClaw这类工具就会发现,它的价值根本不在“聊天”,而在“执行”。

OpenClaw在我的理解里,本质上是一个“个人终端大管家”式的Agent框架。你不用把任务拆成一步步指令告诉它,只需要说“帮我查一下某个目录下最近三天改过的文件,把列表整理成Markdown发出来”,它就会自己去翻文件系统、调用搜索工具、组织语言、输出一篇带格式的结果。这件事的关键不是“听懂人话”——现在的大模型早就听得懂了——而是“把听懂的话变成一连串可控的动作”。

这就引出了Agent和聊天机器人的分水岭:Agent是有行动闭环的。它不再只在语言空间里打转,而是把语言理解作为输入,把操作真实世界的工具作为输出,再根据工具返回的结果决定下一步动作。你要复刻OpenClaw,首先要复刻的就是这个“理解—行动—观察—再理解”的闭环,也就是业内常说的Agentic Loop。

1.1 从“问答机器人”到“会干活的助手”

举一个生活化的例子。你招了一个实习生,让他整理一份会议纪要。如果他是个普通问答机器人,他会给你一份“如何整理会议纪要”的方法论,写得很好,但什么也没做。如果他是个Agent,他会打开笔记软件、找到原始录音、转写文字、按议题分条、导出成文档,然后把文件路径发给你。

这两者的差距,就是“知道怎么做”和“真的去做”之间的差距。OpenClaw这类框架要解决的,就是把后者自动化。

要实现这种自动化,模型本身只负责“做决策”:决定下一步该调哪个功能、以什么参数调用、以及什么时候收手。真正“动手”的,是一系列被封装好的工具函数:读文件、写文件、查数据库、发HTTP请求、执行终端命令。模型和工具之间需要一个调度中枢,负责传递信息、记录中间状态、判断循环是否该终止。

复刻OpenClaw的第一步,不是先去啃复杂的依赖注入、事件总线、插件系统,而是先把这个“调度中枢”写出来。你会发现,去掉一切工程包装之后,核心逻辑其实非常朴素,朴素到大约100行代码就够用。

1.2 Agentic Loop:Agent的命根子

所谓Agentic Loop,标准说法是ReAct模式(Reasoning + Acting)的工程化实现。大致流程是四步:

  1. 系统拿到用户的原始请求,连同上下文、可用的工具清单一起丢给模型。
  2. 模型返回一段结构化输出,里面包含它的思考过程和下一步动作。动作通常是“调用某个工具,参数是什么”。
  3. 系统执行这个工具,拿到返回值。
  4. 返回值继续作为上下文的一部分交给模型,模型再决定下一步做什么,如此循环,直到模型输出一个“结束”标记。

这四步听上去简单,但里面藏着Agent工程化的几乎所有麻烦点。比如:模型输出不是合法JSON怎么办?工具执行报错怎么往回传?循环次数有没有上限?上下文会不会越滚越长导致模型“失忆”?这些问题在OpenClaw这种成熟框架里都有处理策略,但它们的源头,都来自这个看似简单的循环。

想理解Agent,最笨也最有效的方法,就是亲手把上面四步写成代码。等你写完了,再回头看LangChain、PydanticAI这类框架,就会觉得它们只是在帮你做工程封装,而不再是神秘的黑魔法。

1.3 OpenClaw这种框架到底省了你什么

既然核心循环只要100行,那OpenClaw这种框架存在的意义是什么?我的理解是,它节省的是“围绕核心循环的周边工程”,而不是“核心循环本身”。

周边工程包括:工具的动态加载系统,让你往里加一个新工具不用改主循环代码;记忆管理模块,让Agent能记住长时间对话里的关键信息;多智能体协作路由,让不同职责的子Agent协同工作;安全审计和权限控制,限制Agent对系统的访问边界;以及面向不同平台的接入适配层。

如果你是一个想在生产环境里快速搭Agent服务的人,直接用成熟框架完全合理。但如果你是“想变精通”的人,我的建议是做一次逆向工程式的复刻:自己写一遍最小核心,再回头去读框架源码,你会有一种“原来它这里是为了解决我踩过的那个坑”的顿悟感。

这就是这篇博文的意义所在。

2. 核心设计:一个最小Agent需要哪几块积木

在动手写代码之前,先把地图画出来。我用的是“积木化”的思路:一个最小Agent,拆到底就是四块积木——模型层、工具层、记忆层、编排层。把这四块搞清楚,100行代码就水到渠成了。

2.1 模型层:把“对话模型”包装成“决策模型”

模型层解决的是“怎么和LLM打交道”。大部分兼容OpenAI协议的模型服务都会提供一个ChatCompletion式的接口。你可以传一段对话历史(messages列表),每一条消息带角色(system/user/assistant)和内容(content),模型返回一条新的assistant消息。

在Agent场景里,仅仅拿到一条自然语言回复是不够的。你还得让模型在回复里“夹带”结构化指令,比如“调用check_weather工具,参数city=Beijing”。业界常见做法是Function Calling。简单说就是:你把工具的名称、描述、参数结构一并传给模型,模型有能力直接返回一段结构化的函数调用请求,而不是把调用意图写在自然语言里。

如果你用的模型服务不支持原生Function Calling,也有替代方案:让模型输出JSON,游你用正则捞出来再解析。这种方案对模型的指令遵循能力要求更高,效果相对不稳定,但胜在通用。我会在后面的代码里把这两种情况合并处理:优先用原生的工具调用字段,拿不到再尝试解析JSON。

模型层还需要考虑一个重要问题:温度参数和采样参数。Agent场景下,我不建议把它调得太高。对话创作可以追求“惊喜感”,但Agent执行任务要的是稳定和可控。有的框架默认温度是0.7,在Agent场景里我会果断压到0.2甚至0。你是在让模型当执行器,不是在让它当诗人。

2.2 工具层:给Agent装上手和脚

工具层负责“Agent能做什么”。无论是OpenClaw还是别的Agent框架,工具层的设计方案都出奇一致:一个函数,一份JSON Schema描述。

函数是实际执行者,JSON Schema是给模型看的“使用说明书”。模型不直接调用你的Python函数——它不可能执行你的代码——它只是根据说明书生成一个“调用请求”。真正的执行发生在你的进程里:解析模型给出的函数名和参数,然后映射到对应Python函数执行。

所以工具层要做的核心事情有三件:

  • 定义一个工具类或装饰器,把Python函数和它的JSON Schema绑定起来。
  • 提供一个“工具注册表”,让Agent能查询当前有哪些工具可用。
  • 提供一个“执行器”,根据模型传来的调用请求,去注册表里找到函数并执行。

这里有个容易被忽略的细节:工具描述写得越具体,模型调用得越准。我见过很多人的工具调用不准,不是模型不行,而是他的工具描述写得太含糊。比如一个search_web工具,描述里应该写清楚“搜索当前互联网,适合查询事实性信息、新闻、人物资料。参数query为搜索关键词,建议使用简洁名词短语,而非完整句子”。这样的描述能显著提升模型选工具的准确率。

还有一个工程细节值得提:工具函数的参数校验。模型传参偶尔会传错类型,比如把city写成数字,或者把必填参数漏掉。这时候执行器要能捕获异常,把错误信息原样返回给模型,让模型自己纠错。这个“容忍错误并继续循环”的能力,是Agent具备鲁棒性的关键。

2.3 记忆层:短期会话与长期上下文

记忆层负责“Agent能记住什么”。把它放在第三块积木的位置,是因为它直接决定了Agent能不能处理多轮复杂任务。

最小实现里,记忆可以简单理解成一个messages数组。每一轮循环,把用户的提问、模型的思考、工具的返回都追加到这个数组里。下一轮请求时,整个数组一起发给模型。模型因此能看到“我之前做了什么”“工具返回了什么”“现在进行到哪一步”。

这个设计简单粗暴,但有一个致命问题:上下文会无限膨胀。假设工具返回了100行日志,下一步模型调用再返回100行,三五个循环之后,上下文可能就顶到模型窗口的天花板了。解决思路有几种:

  • 截断:只保留最近N条消息。
  • 摘要:把历史消息交给模型总结成一段话,替换原内容,这也是“Memory”常见做法。
  • 结构化存储:把工具返回结果里的关键字段抽出来,存成简短的状态记录;原始详细数据放在外部存储。

对于100行的最小实现,我会把截断和摘要的“插口”留出来,但默认只做截断,避免代码量失控。你理解了这个核心,后续想升级就有明确方向了。

2.4 编排层:循环、停止条件与异常处理

最后一块积木,也是“灵魂”所在:编排层。它干的事情,就是把模型层、工具层、记忆层串起来,形成一个可终止的循环。

循环的控制权在模型手里。模型返回一个动作,你执行动作;模型返回一个结果,你把它交给用户。什么时候停止?业界常用的停止条件有三个:

  1. 模型没有产生任何工具调用请求,说明它认为任务已经完成。
  2. 模型输出的内容里明确包含结束标记(比如[DONE])。
  3. 循环次数达到预设上限(比如10次),强制终止,避免Agent陷入死循环。

第三种情况非常重要。模型有时候会抽风,在一个错误的工具调用上反复打转。设置max_iterations是最便宜的保险丝。

异常处理也要放在编排层。工具调用可能抛异常、网络请求可能超时、模型接口可能限流。我的策略是:把所有异常捕获后转成普通文本,作为工具执行结果的一部分返回给模型。让模型看到“调用失败了,原因是xxx”,它就能决定是换一种参数重试、换一个工具、还是向用户求助。这种“把异常变成上下文”的设计,在Agent领域非常实用,它让Agent具备了自我修复的可能性。

3. 实操:100行Python代码搭出你的Agent骨架

现在进入正题。下面这份代码是我在模拟项目X里实际跑通过的一个最小Agent骨架,面向终端场景设计,去掉了UI和插件系统,只保留核心链路。它是一个Agent最新版的“最小可复刻样本”——你能看到消息协议、模型调用、工具调度、循环终止以及错误处理是怎么在一个文件里共存的。

3.1 准备环境与配置

在写代码之前,准备三样东西:

  • Python 3.10以上,因为代码里用到了类型注解的现代写法。
  • 一个兼容OpenAI协议的模型服务地址和API Key。无论你用本地跑的模型,还是某云厂商的托管服务,只要对方提供OpenAI兼容的接口,统一走下面的代码就行。
  • openai这个Python库,版本比较新(建议1.x)。如果不想装这个库,也可以用requests直接调用HTTP接口,代码量会多一点,但自由度更高。

我这边测试用的环境是:Python 3.11,openai1.35.0。模型参数我用的是model="your-model"这个占位符,你用的时候替换成自己实际可用的模型名。

3.2 核心代码逐段拆解

下面是完整脚本,我把它起了个名字叫mini_agent.py。建议你新建一个文件直接复制进去,先跑通,再逐行理解。

import json import os import time from typing import Any, Callable, Dict, List, Optional, Tuple class Tool: """工具类:绑定一个执行函数和一份JSON Schema描述。""" def __init__(self, name: str, description: str, parameters: dict, func: Callable): self.name = name self.description = description self.parameters = parameters self.func = func def run(self, **kwargs) -> str: try: result = self.func(**kwargs) if isinstance(result, str): return result return json.dumps(result, ensure_ascii=False) except Exception as e: return f"[工具执行错误] {type(e).__name__}: {e}" def get_current_time() -> str: """返回当前本地时间。""" return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) def list_directory(path: str) -> str: """列出指定目录下的所有条目,返回JSON数组字符串。""" if not os.path.isdir(path): raise FileNotFoundError(f"目录不存在: {path}") entries = os.listdir(path) return json.dumps(entries, ensure_ascii=False) class AgentRegistry: """工具注册表:负责记录和查询可用的工具。""" def __init__(self): self._tools: Dict[str, Tool] = {} def register(self, tool: Tool): self._tools[tool.name] = tool def get(self, name: str) -> Optional[Tool]: return self._tools.get(name) def get_schemas(self) -> List[dict]: schemas = [] for tool in self._tools.values(): schemas.append({ "type": "function", "function": { "name": tool.name, "description": tool.description, "parameters": tool.parameters, }, }) return schemas class OpenClawAgent: """极简Agent:串起模型、工具、记忆、循环。""" def __init__(self, model: str, base_url: str, api_key: str, max_iterations: int = 10): # 模型服务客户端,openai库会自动识别base_url from openai import OpenAI self.client = OpenAI(base_url=base_url, api_key=api_key) self.model = model self.max_iterations = max_iterations self.registry = AgentRegistry() def register_tool(self, name: str, description: str, parameters: dict, func: Callable): self.registry.register(Tool(name, description, parameters, func)) def _call_model(self, messages: List[dict]) -> Tuple[str, Optional[dict]]: """调用模型,返回(回复文本, 函数调用请求)。优先使用原生function calling。""" tools = self.registry.get_schemas() kwargs = {} if tools: kwargs["tools"] = tools kwargs["tool_choice"] = "auto" resp = self.client.chat.completions.create( model=self.model, messages=messages, temperature=0.2, **kwargs, ) msg = resp.choices[0].message if msg.tool_calls: call = msg.tool_calls[0] # 解析函数名和参数,参数可能被编码为JSON字符串 try: args = json.loads(call.function.arguments or "{}") except json.JSONDecodeError: args = {"_raw_args": call.function.arguments} return msg.content or "", { "name": call.function.name, "arguments": args, } return msg.content or "", None def run(self, user_query: str) -> str: """Agent入口。返回Agent处理完用户请求后的最终文本回答。""" messages = [ {"role": "system", "content": "你是终端智能助手。你可以使用工具完成任务。" "如果需要调用工具,直接调用;得到结果后继续判断。" "等你确认任务已经完成,再给出最终回复。"}, {"role": "user", "content": user_query}, ] for step in range(self.max_iterations): print(f"\n--- 第 {step + 1} 轮 ---") # 1. 调用模型,得到回复和可能的工具调用请求 reply, tool_call = self._call_model(messages) # 2. 没有工具调用,说明模型认为任务已完成,收工 if tool_call is None: return reply if reply else "任务完成,但没有生成文本回复。" # 3. 有工具调用则执行 tool_name = tool_call["name"] tool_args = tool_call["arguments"] print(f"调用工具: {tool_name}, 参数: {tool_args}") tool = self.registry.get(tool_name) if tool is None: result = f"[错误] 未知工具: {tool_name}" else: result = tool.run(**tool_args) print(f"工具返回: {result[:200]}") # 4. 把助手回复、工具调用、工具结果,都追加进消息历史 messages.append({ "role": "assistant", "content": reply or "", "tool_calls": [ { "id": f"call_{step}", "type": "function", "function": { "name": tool_name, "arguments": json.dumps(tool_args, ensure_ascii=False), }, } ], }) messages.append({ "role": "tool", "tool_call_id": f"call_{step}", "content": result, }) return f"[已达最大循环次数 {self.max_iterations} 次,强制终止]" def main(): # 从环境变量里读取模型配置,而不是写死在代码里 agent = OpenClawAgent( model=os.getenv("MODEL_NAME", "your-model"), base_url=os.getenv("MODEL_BASE_URL", "http://localhost:8000/v1"), api_key=os.getenv("MODEL_API_KEY", "sk-local"), ) # 注册两个简单的演示工具 agent.register_tool( name="get_current_time", description="获取当前本地时间。用户询问现在几点、日期时使用。", parameters={ "type": "object", "properties": {}, "required": [], }, func=get_current_time, ) agent.register_tool( name="list_directory", description="列出指定目录下的所有文件和子目录名。当用户要求查看目录内容时使用。", parameters={ "type": "object", "properties": { "path": {"type": "string", "description": "要列出的绝对路径或相对路径。"}, }, "required": ["path"], }, func=list_directory, ) query = "现在几点了?顺便帮我看看当前目录下有哪些文件。" print(agent.run(query)) if __name__ == "__main__": main()

这段代码我数过,去掉注释和空行,刚好在100行上下浮动。它的核心链路和OpenClaw这类框架是一致的:系统指令给模型立人设,工具注册表给模型派发技能清单,主循环负责调度和终止。你完全可以把这段代码当做一个微型OpenClaw来理解。

3.3 让它跑起来:一个联网搜索加文件操作的Demo

上面的代码里注册了两个本地工具,跑一遍输出大致是这样的:

--- 第 1 轮 --- 调用工具: get_current_time, 参数: {} 工具返回: 2025-06-19 14:32:08 --- 第 2 轮 --- 调用工具: list_directory, 参数: {'path': '.'} 工具返回: ["README.md", "mini_agent.py", "data", "logs"] --- 第 3 轮 --- 最终回答: 当前时间是2025年06月19日14:32:08。当前目录下有以下文件:README.md、mini_agent.py、data文件夹、logs文件夹。

你看,模型自己拆解了两个子任务:先查时间,再列目录,最后把结果组织成回答。没有人在代码里写死这个两步骤流程,是模型根据系统提示和工具清单自己编排出来的。

如果你想做一个更像OpenClaw的联网搜索版本,只需要再注册一个web_search工具:内部调用某个通用搜索服务的API,把返回的标题和摘要截断后返回给模型。注册方式跟上面完全一样。这也是Agent框架最爽的地方:能力可以像插件一样无限扩展,主循环一行都不用改。

4. 从复刻到超越:真正的护城河不在代码量

复刻完成只是起点。文章标题是“从小白到精通(一)”,那这个系列的精通路线,后面的“二、三、四”该往哪个方向走?先把地图画出来。

4.1 100行之后先别急着加功能

我的建议是,先把已有的这个骨架“用熟”,不要急着加几十个工具。

用熟的意思是:让它处理各种真实任务,观察它在哪里掉链子。我做了半年多的Agent开发,最大的体会是——Agent的智商瓶颈往往不在模型,而在“信息通路”和“反馈质量”。

举一个真实场景。让Agent去整理一份销售数据报告。它需要调用一个数据分析工具,工具返回了一大段CSV文本。模型看了之后,说“数据中存在异常值”,但说不清是哪个字段、什么范围的异常。这不是模型笨,而是工具返回的信息过于原始,没有按Agent的需求做结构化。

这时候真正应该做的不是换模型,而是改进工具的返回格式。让数据分析工具多返回一个“字段摘要”段落,把最大值、最小值、缺失值数量直接标注出来。反馈质量一提升,Agent的决策质量立刻跟着提升。在100行代码里,你不需要做复杂的记忆管理或微调,只靠“精心设计工具的输入输出”,就能让Agent的可用性翻倍。

4.2 工具调用失败、模型抖动、上下文爆炸的实战排查

这部分是实战里没法绕开的三个坑,我一个个说。

第一个坑,工具调用失败。模型返回了函数调用,但你的函数抛了异常。上面的代码已经把异常catch住并转成了文本返回。但要注意,返回给模型的错误信息不能只是“list_directory失败”,而要带上具体原因,比如“Permission denied: /root”。模型能看到原因,才能自行决定是换个路径,还是告诉用户没有权限。把错误信息写得越具体,Agent的自愈能力越强。

第二个坑,模型抖动。模型状态不稳定,同一套指令,前三轮回答质量都很高,第四轮开始乱来,工具参数格式都变了。这时不要硬调prompt,先看是不是上下文太长了,扰乱了模型对指令的遵循。多数情况下,把历史消息里早期的长文本做摘要压缩,症状就会消失。

第三个坑,上下文爆炸。这个上面已经提过。100行版本里我没做压缩处理,但在生产环境里必须做。给你一个可以直接用的妙招:给每条消息打一个“优先级”标记,system提示词和最近的用户消息优先级最高,工具的长返回结果优先级最低。当上下文接近窗口上限时,从最低优先级开始删,直到腾出空间。这比无脑截断最后N条效果好得多。

4.3 评测与观察:怎么知道Agent“变聪明了”

Agent开发最痛苦的一点,是没有一个像“准确率”那样一目了然的指标。你改了一个prompt,感觉Agent变聪明了,但又说不清具体哪里变好了。这很正常,因为Agent的智能是系统性的,不是一个分数能概括的。

我给自己的项目搭了一套极简评测方案:准备10到20个贴近真实使用场景的任务,跑一遍Agent,看三个指标:

  • 任务完成率:最终结果能否满足用户需求。判定标准可以适当放宽,主要看出没出活。
  • 工具调用有效率:Agent调用工具的次数里,多少是真正有用的、多少是瞎试的。
  • 平均循环轮数:完成一个任务平均需要几步。轮数太高,说明Agent在迷茫地打转。

你改了任何一行核心逻辑,都跑一遍这套评测。前后对比,不要看“感觉”,要看数字。这也是“从小白到精通”最关键的一步——从凭感觉调试,变成用数据调试。一旦跨过这个坎,你的Agent迭代速度会快上很多。

5. 常见问题与排查技巧实录

最后把这几个月做Agent复刻过程中遇到的典型问题整理成一份速查表。每个问题都是我实际踩过的,不是从文档里抄的。

现象可能原因排查与解决
Agent反复调用同一个工具,始终不结束工具返回的信息没有帮助模型做出新判断,模型陷入循环检查工具返回值是不是过于简单;设置max_iterations兜底;在工具返回里增加“下一步建议”字段
模型返回的工具参数解析失败模型输出非法JSON,或者参数里混入了特殊字符捕获JSONDecodeError,把原始字符串传给模型纠错;在工具定义里加严格枚举约束,减少模型发挥空间
Agent明明有工具可用,却不调用模型服务不支持function calling,或者系统prompt没写清楚“可以调用工具”先用API文档确认工具调用开关;没有原生支持时,在prompt里强制要求输出JSON动作
多轮对话历史一长,Agent就“失忆”上下文超长导致注意力分散把早期长文本压缩成摘要;只保留最近N轮“用户-助手-工具”消息
工具执行很快,但整体响应很慢模型每轮都要重新读取全部上下文,工具执行只是很小一部分减少历史消息里的重复内容;用摘要代替原始返回;考虑将工具结果存入外部缓存,引用缓存ID
Agent给出的最终回答格式很糟糕系统prompt没有规定输出格式在system消息里明确“最终回答必须包含结论在前、细节在后”,甚至给它一个模板

表格之外,再分享一个独家技巧。很多人写工具描述时,只写“这个工具是干什么的”,不写“什么时候不该用它”。比如list_directory的描述,我除了写“列出目录内容”之外,还会补一句“如果用户只是想查询文件内容,请改用read_file工具,而非本工具”。这种“负向提示”能明显减少工具误调用的概率。理由很简单:模型在工具选择上靠的是语义匹配,你给的信息越具体,它的匹配准确率越高。

还有一个感受很深的地方:调试Agent时,一定要把“中间过程日志”打出来。很多人只关注最终结果对不对,但Agent的中间步骤才是信息最丰富的地方。我上面代码里每次循环都有print(f"调用工具: {tool_name}"),这个习惯关键时刻能救命。排查问题时先翻中间日志,判断是哪一步决策出了问题,再回去改prompt或工具定义,效率高得多。


做Agent做得越久,我越觉得“像一个会干活的人”这个类比比“像一个会聊天的机器人”准得多。100行代码复刻OpenClaw,不是为了证明代码短,而是为了让你亲手把那个听着很玄的Agent黑盒打开。你看过它的骨架,知道它每个环节的强弱项,之后再遇到生产环境里的各种Agent框架,就不会再被包装术语搞得发懵了。下一步,我个人建议你试着在这个骨架里加一个真正实用的工具,比如一个能直接操作数据库的查询工具。等到你能让Agent自己决定“先查A表还是先查B表、根据查询结果决定要不要继续查C表”的时候,你基本上就出师了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询