月初的时候,一位做企业服务的朋友专程跑来找我,说他们客户那边有三个岗位,天天干的事几乎一模一样:接工单、查数据、填表格、回复结果。他想让我用 OpenClaw 这个企业级智能体框架帮他们试试,看能不能把其中一个岗位的日常流程真正自动化。我跟他说,能,但你要先接受一个前提——它不是那种你问一句它答一句的问答机器人,而是一个需要你给工具、给权限、给边界、给复查机制的“AI员工”。
这篇文章是 OpenClaw 企业级智能体实战系列的第一篇,我会把从零搭建第一个 AI 员工的完整过程摊开讲,包括原理、算法、完整代码和避坑指南。适合两类人看:一类是刚接触智能体、想弄明白它到底怎么“干活”的开发新手,另一类是已经在做企业交付、想直接抄一套能落地的骨架方案的人。先说好,这篇不会讲花哨的 Agent 框架功能清单,只讲让一个 AI 员工真正把事做完的最小闭环。
1. 我理解的“AI员工”:一个能独立处理任务闭环的智能体系统
很多团队一上来就喊“我们要做 AI 员工”,结果做出来个对话框。这中间的落差,不是模型的问题,而是对“AI员工”这个定位的理解问题。
1.1 AI员工和问答机器人的本质区别:闭环
问答机器人的核心是一个“对话回合”:用户说一句,模型回一句,这个过程没有状态、没有行动、没有交付。AI员工不一样,它承担的是一个完整的任务闭环:接收目标,拆解步骤,调用工具,检查结果,最后交付产出。
打个比方。普通对话 AI 像是一个很能聊的顾问,只动嘴,从不动手。而 AI 员工像是一个刚入职第一天的实习生:它一开始并不知道你们公司内部系统长什么样,但只要你在入职第一天给它账号权限、给它操作手册、给它审批流程,它就能照着 SOP 一步一步把事做完。OpenClaw 要做的,就是把这套“入职引导”变成代码。
所以在我写任何一行程序之前,一定会先问自己一个问题:这个 AI 员工负责的任务,最末端那个“交付物”到底是什么?是填好的表格、发出去的邮件,还是已经更新到业务系统里的订单状态?只要交付物不明确,后面肯定做偏。
1.2 什么样的岗位适合升级成AI员工
不是所有岗位都能直接换成 AI 员工,盲目上只会把交付项目做成事故现场。我一般用下面这张表来快速判断:
| 岗位特征 | 适合程度 | 原因 |
|---|---|---|
| 规则明确、流程固定 | 非常适合 | 工具调用的每一步都可预期,不会出现模型自由发挥的空间 |
| 高频重复、单量巨大 | 非常适合 | 边际成本低,自动化的收益非常明显 |
| 需要大量内部系统查询 | 适合 | AI 擅长格式化检索和汇总,比人翻系统快得多 |
| 涉及复杂人际谈判 | 不适合 | 目前技术还无法替代信任关系的建立 |
| 需要对结果负法律责任 | 不适合 | 模型输出的“合理性”不等于合规性,兜底机制尚未成熟 |
| 需要模糊战略判断 | 不适合 | 模型没有真正的“判断力”,只有模式匹配能力 |
这里有一个经常被忽略的点:AI员工最合适的岗位,往往不是最复杂、最聪明的岗位,而是最无聊、最重复的岗位。因为只有无聊的岗位,才存在足够稳定的 SOP 可以写成代码。
1.3 一个典型场景:售后工单的预处理流程
我实际操作过的某个案例,是一家电商公司的售后工单中心。他们的工单日处理量在三千条左右,原来需要四个人做粗分:读工单文本,判断问题类型,标记紧急程度,提取订单号,再去 CRM 系统里查一下用户等级,最后推给对应的处理组。单条工单人工处理大概三分钟。
用 OpenClaw 搭了一个“工单预处理员工”之后,流程变成这样:读取工单文本,调用分类模型接口,调用 CRM 接口查用户信息,再按照预设路由规则分发。单条耗时压到了四十秒左右,而且每一步都有日志可以追查。这种岗位就是最典型的 AI 员工落地场景——不需要它做重大判断,只需要它把一整套确定性流程稳定、可审计地执行下去。
2. OpenClaw的底层机制:Agent循环、工具调用与记忆是怎么协同的
聊完了定位,进入原理。AI员工不是魔法,它的底层就是一个很朴素的循环。理解这个循环,后面调参、排错才会有方向感。
2.1 核心思想:观察-决策-行动的循环
OpenClaw 这类智能体框架,核心机制其实是“ReAct”模式,也就是让大模型一边推理一边行动。整个运行过程可以抽象成下面几个步骤:
- 接收用户任务或系统事件。
- 大模型根据当前上下文输出下一步动作。这个动作有两种可能:要么是直接给最终回复,表示“任务我搞完了”;要么输出一个 JSON 格式的指令,表示“我要调用某个工具”。
- 运行时解析这个输出,在工具注册表里找到对应工具,执行它。
- 把工具执行结果作为新的上下文,再次交给大模型。
- 重复,直到模型认为任务完成,或者到达我们设定的最大步骤数。
你可以把它想象成开车导航。导航负责的是“决策”:前方三百米右转、靠左行驶。执行这个决策的,是方向盘、油门和刹车。大模型就是导航,工具就是方向盘;OpenClaw 干的事情,就是确保导航说“右转”的时候,方向盘真的会自己转,而不是仅仅显示在屏幕上。
2.2 工具调用协议:让AI可以“操作”而不是“聊天”
可能有人会好奇:模型怎么就能输出一个结构化的工具调用指令?原理其实不神秘。大模型的本职工作就是生成文本,只要你在系统提示词里跟它约定好规则——当它觉得需要某个能力时,输出一个特定格式的 JSON,里面包含两个字段:tool表示工具名,args表示参数——它就能照着输出。剩下的就是代码去解析和调用的问题。
在企业级项目里,我坚持自己实现这一层工具调用协议,而不是直接依赖某个大模型服务商自带的 function calling 能力。原因很简单:统一协议意味着统一治理。不管模型服务供应商怎么换,我的工具注册、权限校验、参数校验、日志审计、失败兜底,全部基于同一个格式。换模型只改一个函数,换工具只加一条注册,不会动整个框架。
2.3 上下文与记忆:短期窗口与长期知识的分工
AI 员工在工作过程中,每调用一次工具,就需要把“模型说的话”和“工具返回的结果”追加到对话历史里。如果不加控制,上下文会像滚雪球一样越滚越大,最后超过模型的窗口长度,或者导致响应越来越慢、越来越贵。
这里的解决方案分两层。短期上下文,我们要做“最近 N 轮”的滑动窗口管理,超出范围的旧对话要么丢弃,要么压缩成摘要;长期知识,则要引入企业知识库,按需检索后注入到对话里。很多 AI 员工用着用着“变傻”,其实不是模型不行,而是上下文管理没做好——它被一万个历史细节淹没,压根不记得自己现在正在干什么。
3. 第一个AI员工落地全流程:环境、依赖与最小可用代码
原理部分到此为止,下面开始真正的代码环节。我尽量让最小骨架能跑起来,同时刻意不绑定任何特定的大模型 SDK,方便你直接接入自己的模型服务。
3.1 环境准备:Python版本、依赖与目录结构
运行这个骨架只需要 Python 3.10 以上版本,第三方依赖只需要requests或httpx这类最小的 HTTP 库。建议目录结构如下:
your_project/ ├── agent_core.py # Agent骨架:工具注册、记忆、运行循环 ├── main.py # 启动入口:注册工具、执行任务 ├── audit.log # 运行日志 └── tests/ # 回归测试(后面避坑部分会讲到)为什么提倡轻依赖?因为企业里的模型服务是会换的。今天可能用 A 家的模型,明天可能因为成本换 B 家的,后天可能换成私有化部署的开源模型。如果你的代码层直接写死了某家的 SDK,到时候改起来就是牵一发动全身。所以我会在代码里做一个chat_fn抽象层,它只负责做一件事:把 messages 数组发给模型服务,把文本响应拿回来。
3.2 核心代码:一个带工具调用能力的最小Agent骨架
先写核心的agent_core.py,这里面包含了工具注册表、上下文记忆和主循环三个部分。注释我写得比较细,你可以直接照着敲:
# agent_core.py import json from typing import Any, Callable, Dict, List, Optional class ToolRegistry: """工具注册表:统一管理AI员工能用的所有工具。""" def __init__(self): self._tools: Dict[str, Dict[str, Any]] = {} def register( self, name: str, description: str, func: Callable, parameters: Dict, ): """注册一个工具。parameters是JSON Schema格式的参数说明。""" self._tools[name] = { "description": description, "func": func, "parameters": parameters, } def list_tools(self) -> str: """生成给模型看的工具列表文本。""" lines = [] for name, meta in self._tools.items(): lines.append(f"- {name}: {meta['description']}") return "\n".join(lines) def call(self, name: str, args: Dict): """执行工具。这里后续会加入参数校验,见第4章。""" meta = self._tools.get(name) if not meta: raise KeyError(f"未注册的工具: {name}") return meta["func"](**args) class AgentMemory: """一个最简单的上下文管理器:保留最近N轮对话。""" def __init__(self, max_rounds: int = 5): self.max_rounds = max_rounds self._messages: List[Dict[str, str]] = [] def append(self, role: str, content: str): self._messages.append({"role": role, "content": content}) # 超出轮数就丢最早的两条,防止上下文无限膨胀 if len(self._messages) > self.max_rounds * 2: self._messages = self._messages[-(self.max_rounds * 2):] def history(self) -> List[Dict[str, str]]: return self._messages class AIEmployee: """一个最小可用的AI员工。""" def __init__(self, chat_fn): # chat_fn(messages) => str,这个函数负责对接真实的大模型服务 self.chat_fn = chat_fn self.tools = ToolRegistry() self.memory = AgentMemory() self._system_prompt = "" # 延迟到run时生成,因为那时代理工具才注册完 def _default_prompt(self) -> str: return ( "你是一个企业级AI员工。你可以使用以下工具:\n" f"{self.tools.list_tools()}\n" "当你认为需要调用工具时,请只输出如下格式的JSON:\n" '{"tool": "工具名", "args": {"参数1": "值1", "参数2": "值2"}}\n' "如果任务已经完成,请直接输出给用户的最终回复。" ) def run(self, task: str, max_steps: int = 10) -> str: if not self._system_prompt: self._system_prompt = self._default_prompt() messages = [ {"role": "system", "content": self._system_prompt}, ] self.memory.append("user", task) for step in range(max_steps): history = messages + self.memory.history() reply = self.chat_fn(history) action = self._parse_action(reply) if action is None: self.memory.append("assistant", reply) return reply # 模型认为任务完成,直接返回 try: result = self.tools.call(action["tool"], action["args"]) except Exception as e: result = f"调用工具失败: {e}" self.memory.append("assistant", f"调用工具 {action['tool']},参数 {action['args']}") self.memory.append("tool", json.dumps(result, ensure_ascii=False)) return "达到最大步骤数,任务未完成,请人工介入。" def _parse_action(self, reply: str) -> Optional[Dict]: """解析模型输出。优先解析纯JSON,失败则尝试从代码块中提取。""" reply = reply.strip() if reply.startswith("{"): try: data = json.loads(reply) if data.get("tool"): return data except json.JSONDecodeError: pass # 兼容模型喜欢输出```json代码块的情况 if "```json" in reply: start = reply.find("```json") + 7 end = reply.find("```", start) if end == -1: return None try: data = json.loads(reply[start:end].strip()) if data.get("tool"): return data except json.JSONDecodeError: return None return None注意到一个细节没有:_system_prompt不是初始化时就生成,而是等到run()的时候才生成。这是因为提示词里要包含工具列表,而工具是在初始化之后才手工注册的。如果顺序反了,AI 会以为自己没有任何工具可用,只会跟你聊天。这个顺序问题,很多新手写的时候都会踩到。
3.3 第一次运行:注册两个典型工具并实测
接下来写启动入口main.py,注册两个最典型的工作工具:查订单、算折扣。这里我特意放了一个fake_chat_fn,它用本地逻辑模拟模型行为,目的是让你在没有真实模型 API 的情况下也能完整体验一遍 Agent 循环。
# main.py import json from agent_core import AIEmployee def fake_chat_fn(messages): """本地模拟大模型,仅用于演示Agent循环逻辑。""" last_user_content = messages[-1]["content"] if messages[-1]["role"] == "user" else "" last_tool = [m for m in messages if m["role"] == "tool"] if "查询订单" in last_user_content: return '{"tool": "query_order", "args": {"order_id": "A1001"}}' if last_tool: return "订单A1001状态:已发货,物流公司:测试物流,单号:SF123456" return "请告诉我你需要查询的内容。" def real_chat_fn(messages): """对接真实大模型服务的入口。 在实际项目中,你只需要在这里把messages数组发给任何兼容 标准对话协议的大模型服务,然后把文本响应返回即可。 """ # payload = { # "model": "your-model-name", # "messages": messages, # "temperature": 0.2, # } # resp = requests.post(LLM_API_URL, json=payload, headers=HEADERS, timeout=30) # return resp.json()["choices"][0]["message"]["content"] pass # 示意占位,正式使用时替换为真实的HTTP调用 def query_order(order_id: str) -> dict: """对接真实订单系统的查询函数。""" return { "order_id": order_id, "status": "已发货", "logistics_company": "测试物流", "tracking_no": "SF123456", } def calc_discount(amount: float, member_level: str) -> dict: """根据会员等级计算折扣。""" rate = {"普通": 0.0, "银卡": 0.05, "金卡": 0.1}.get(member_level, 0.0) discount = amount * rate return { "original_amount": amount, "discount": discount, "pay_amount": amount - discount, } if __name__ == "__main__": agent = AIEmployee(chat_fn=fake_chat_fn) # 注册工具:名称、描述、函数、参数schema agent.tools.register( "query_order", "根据订单号查询订单状态,返回物流信息", query_order, {"type": "object", "properties": {"order_id": {"type": "string"}}}, ) agent.tools.register( "calc_discount", "根据金额和会员等级计算折扣后的应付金额", calc_discount, { "type": "object", "properties": { "amount": {"type": "number"}, "member_level": {"type": "string"}, }, }, ) result = agent.run("帮我查一下订单A1001的状态") print("AI员工输出:", result)运行python main.py,你会看到输出结果类似这样:
AI员工输出: 订单A1001状态:已发货,物流公司:测试物流,单号:SF123456如果加上调试日志,你还能看清这个过程中发生了什么:第一次传给 chat_fn 的任务包含“查询订单”,fake_chat_fn 返回工具调用 JSON;主循环解析后调用query_order;工具结果追加到上下文;第二次传给 chat_fn 时,fake_chat_fn 看到了 tool 结果,于是输出最终回复。这就是一个完整的最小闭环。
4. 企业场景下的关键升级:参数校验、审计日志与稳定运行
最小骨架能跑通之后,距离“企业级”还差得很远。我在做交付时,会优先补齐下面三块能力:参数校验、审计日志、稳定运行。这三块决定了 AI 员工能不能面对真实环境里的脏数据和突发状况。
4.1 用JSON Schema校验工具参数,治住大模型的“随手乱传”
大模型生成的 JSON 是不可信的。它不是结构化程序,它是概率输出,所以它完全可能把一个数字传成字符串,把必填字段漏掉,甚至传一个根本不在 schema 里的参数。如果直接透传给业务函数,轻则 TypeError,重则污染业务数据。
给ToolRegistry加一层参数校验,先判断类型和必填项:
def validate_params(params_schema: dict, args: dict) -> list: """返回参数校验错误列表,空列表表示校验通过。""" errors = [] required = params_schema.get("required", []) for field in required: if field not in args: errors.append(f"缺少必填参数: {field}") props = params_schema.get("properties", {}) for key, value in args.items(): if key not in props: errors.append(f"未知参数: {key}") continue expected = props[key]["type"] if expected == "string" and not isinstance(value, str): errors.append(f"参数 {key} 应为字符串,实际为 {type(value).__name__}") elif expected == "number" and not isinstance(value, (int, float)): errors.append(f"参数 {key} 应为数字,实际为 {type(value).__name__}") return errors然后在ToolRegistry.call里,把校验放在执行工具之前。校验失败时不要直接抛异常中断流程,而是把错误消息返回给模型,让它自己修正参数重新调用。经验是:模型读到自己生成的错误提示后,第二次通常就能传对。
4.2 给AI加“工作日志”:每一步都可回溯
企业里上线一个能自动操作系统的 AI 员工,第一关不是功能测试,是安全审计。审计员会问一句话:“如果它做错了,你们怎么知道是什么时候、因为什么、做了哪个动作?”这就要求 AI 员工必须有工作日志。
简单的实现是加一个AuditLogger:
# audit.py import json from datetime import datetime class AuditLogger: """极简审计日志:记录AI员工的每一步动作。""" def __init__(self, path="audit.log"): self.path = path self.events = [] def log(self, event_type: str, payload: dict): self.events.append({ "time": datetime.now().isoformat(), "type": event_type, "payload": payload, }) # 生产环境建议改成异步批量写入,避免IO阻塞主流程 with open(self.path, "a", encoding="utf-8") as f: f.write(json.dumps(self.events[-1], ensure_ascii=False) + "\n")记录类型建议分成五类:user_request(用户请求原文)、tool_call(AI调用的工具和参数)、tool_result(工具返回结果摘要)、error(异常堆栈)、final_answer(最终回复)。有了这五类日志,任何一次异常都可以追溯到具体是哪一步出了问题、模型基于什么信息做出了决策。
4.3 从“跑通”到“稳定”:重试、超时与并发控制
大模型接口有一个很现实的毛病:不稳定。网络抖动、服务端限流、超时,都是常态。所以模型调用必须包一层重试逻辑。我用的是指数退避策略,第一次等 1 秒,第二次等 2 秒,第三次等 4 秒,最多重试 3 次:
import time def call_llm_with_retry(chat_fn, messages, max_retries=3, timeout=30): for attempt in range(max_retries): try: return chat_fn(messages, timeout=timeout) # 需要你改造chat_fn支持timeout except TimeoutError: if attempt == max_retries - 1: raise time.sleep(2 ** attempt)并发也要控制。AI 员工在线下往往是一个单线程程序,但在企业里它要服务工单队列,所以必须引入线程池或任务队列。我建议最开始时把并发数控制在 4 以内,跑一段时间看模型服务的响应时间和成本,再逐步调大。并发开太高,容易把模型服务打爆,最后所有任务一起超时。
5. 避坑实录:我实测中踩过的六个大坑与完整排查链路
最后这部分是重点。我把实际使用 OpenClaw 过程中踩过的大坑整理出来,每个坑都按照“现象 - 排查链路 - 修复方案”来写。你能复现的不是答案本身,而是排查思路。
5.1 坑一:上下文爆炸——AI越干越“慢”,费用越来越高
现象:有个 AI 员工任务执行到第 20 轮时,每一步的回复时间从最初的 1 秒涨到了 8 秒,一个月 Token 账单直接翻了三倍。
排查链路:我第一件事是去看审计日志。发现主循环每一轮都把全部历史消息发给模型,而工具返回的结果往往几百行 JSON,上下文像滚雪球一样。算笔账:平均每轮新增 800 Token,20 轮之后就是一万六千多 Token,而且每次调用都要把这些全部重新发一遍。时间和费用就是这样涨上去的。
修复:给AgentMemory加滑动窗口策略,最多保留最近 5 轮对话,超过就丢最早的;同时对工具返回结果做截断,超过 1000 字符的只保留关键字段或自动压缩成摘要。要注意的是,只看轮数不够,因为每条消息长短差异很大,生产环境还要再按 Token 数做二次截断。
5.2 坑二:工具调用死循环——AI卡在一个动作上出不来
现象:某次测试中,模型连续 7 次调用同一个查询工具,参数一模一样,每次返回结果都一样,但它依然反复调用,直到max_steps被硬生生截断。
排查链路:看审计日志,发现模型在第三次重复调用后,按理说已经能推断“结果已经稳定,没有新信息”,但提示词里没有任何一条规则告诉它“结果相同就应该结束”。模型的语境里,它只是继续在做“当前最合理的下一步”,并没有意识到自己在空转。
修复:两处同时改。提示词里加一句“当你发现连续两次工具调用参数和结果完全相同时,说明任务没有新进展,请直接给出最终回答”。代码层也加一道保险:跟上一次工具调用比对,相同就注入警告信息,迫使模型跳出循环。
5.3 坑三:参数类型错误——AI把数字当字符串传给业务系统
现象:调用计算折扣工具时,模型把amount传成了字符串"100.5",Python 一执行就抛TypeError,整个流程中断。
排查链路:看日志里存的args,确实就是amount: "100.5"。这个问题的根源在于,模型输出数字时受语言习惯影响很大,中文语境下数字经常被格式化成字符串,尤其是从文本里抽取出来的金额。
修复:靠 4.1 节的validate_params兜底。校验不通过时把明确错误消息返回给模型,并让它重新生成参数,而不是直接中断。实测中,这种错误模型通常在第二次调用就能自行修正。
5.4 坑四:权限失控——AI员工差点执行了高危操作
现象:有一次测试中,用户随口说“帮我把临时数据清掉”,AI 员工竟然真的准备调用删除接口。幸好当时接的是一个假库,否则就是事故。
排查链路:检查权限配置,发现我把三类工具(查询、写入、删除)全部注册给了同一个 AI 员工。模型的“安全判断”是靠提示词约束的,而提示词是软的,不是硬边界。用户只要换个委婉的说法,模型就可能突破它。
修复:把工具分成三个等级:只读类(查询)、写入类(新增、修改)、高危类(删除、审批、转账)。只读工具可以放开给 AI 自动调用;写入工具控制频率并记录日志;高危工具默认不注册给模型,只允许人工触发。这样即使模型被诱导说出“我要删除”,运行时也找不到那个工具。
5.5 坑五:幻觉乱答——AI没查到数据就编了一个结果
现象:有一次查询订单接口返回“查无此单”,模型却给用户编了一个“订单已发货”的状态,用户差点因此安排发货。
排查链路:审计日志显示工具返回了{"error": "order not found"},但模型觉得给用户一个“正常”的答复更重要,于是基于自己的语言习惯编造了一个结果。这在模型术语里叫“幻觉”,本质是它生成的下一句话只是“概率上合理的文本”,而不是“事实”。
修复:双管齐下。提示词写死一条规则:工具返回错误或空结果时,必须如实告知用户查询失败,禁止编造。同时在代码层加强制逻辑:只要工具结果里包含error字段,就自动在上下文中加重标记,提示模型不能编造。
5.6 坑六:测试只看两三个例子——上线第一天就翻车
现象:上线前用 3 个“标准问题”测试,全部通过。结果上线第一天,20% 的工单处理异常。失败的单子几乎全是非标准输入:空订单号、超长文本、用户连续追问、工具超时。
排查链路:标准问题只能验证主流程,而企业环境里搞垮你的从来都是边界和异常。3 个测试用例根本覆盖不到这些场景。
修复:建立回归测试集,至少覆盖五类:正常流程(一路走完)、工具报错(验证模型如实反馈)、参数非法(验证校验和重试)、连续多轮(验证上下文管理)、无关提问(验证 AI 不会越界)。每类写 3 到 5 条用例,接入 CI,每次改提示词或改代码都要跑一遍。这个习惯养成了,后面加新工具时风险会小很多。
最后分享一点个人体会。我搭过好几个 AI 员工,最大的感受是:它能不能稳定工作,不取决于模型有多强,而取决于你愿不愿意把工作流程拆得足够细。工具边界越清晰、兜底机制越完整、日志越全,它就越像个“员工”,而不是一个偶尔抽风的玩具。如果你准备动手,我建议别一上来就接核心系统,先找一个日常最烦的重复流程——查快递、算折扣、汇总日报这种——用上面的骨架跑通一个闭环。真正跑起来之后,你自然就知道下一步该往哪里加了。