1. 动手之前:先想清楚这个助手要帮你干什么
1.1 它到底是个什么东西
OpenClaw 这个项目的核心,并不是让你去训练一个什么大模型,而是帮你搭一个「外壳」:一个能对话、能调用工具、能记住上下文、能通过接口被其他程序驱动的 AI 助手框架。通俗点说,你给它接上一个大语言模型的接口,它就把那些模型能力转化成一个个具体可用的功能。比如你问它“明天下午三点提醒我开会”,它不只是回答你“好的”,而是真的去调用你的提醒服务,给你创建一条待办事项。
我最初看到这个项目标题的时候,以为又是一堆配置文件拼起来的玩具。后来动手才发现,它的思路很实在:所有能力都被设计成“模块 + 工具函数”,你不需要理解模型内部原理,只需要按照约定写好工具、配好提示词,它就自动帮你组装对话循环和函数调用。对只想快速拥有一个专属 AI 助手的人来说,这比从头写代码省了太多事。
你可以把它理解成给电脑请了一个“懂事的实习生”。实习生本身有基础能力(模型),但你得告诉他工作流程(提示词与工具),给他分配具体任务(功能模块),他才能在你的环境里真正干成事。OpenClaw 就是这个“带实习生的管理体系”,而且这套体系完全由你自己掌控。
1.2 我做的第一版需求清单
开始写代码前,我先把第一版想做的事情列成了清单。这步千万别省,因为你一旦开始写你就会发现,AI 助手这个方向可以无限加功能,不加约束迭代会失控。
我当时的第一版需求只有四条。第一条:能在终端聊起来,我发一句它回一句,并且能记住这段对话的上下文。第二条:能调用第三方工具,比如查天气、创建待办、执行一些本地脚本。第三条:通过一个 HTTP 接口暴露服务,这样以后无论写网页、接小程序都能复用同一套后端。第四条:所有配置项都放进一个配置文件里,模型切换、参数调整不用改代码。
这四条需求我刻意做得很克制。没有要求它支持语音、没有要求知识库检索、没有要求多人同时使用。事实证明这种克制太重要了——第一版跑通之后,后面加功能就变得顺理成章。如果你一上来就想着语音、视觉、联网搜索全部一起上,你大概率会在环境配置那一步就放弃。
1.3 哪些功能第一版先不做
同样重要的是想清楚“先不做什么”。我当时主动放弃的有好几项:不做自然语言到代码的自动执行(太危险,模型乱跑命令可能把环境搞乱);不做长时间离线知识库(需要维护向量数据库,成本高但初期价值低);不做多用户权限系统(本地自己用,根本没必要);不做复杂的流程编排(那时候还不确定工具调用能否稳定工作)。
这些设计上的取舍,直接决定了我后面能在一两天时间里就把核心流程跑通。很多做 AI 助手的项目死在过度设计上,不是死在能力不够。你要是有自己的场景,也建议你像我一样先做减法,把主干流程走通,再回头补枝节。
2. 技术选型:这套方案是怎么定下来的
2.1 对话引擎:直接接大模型接口,而不是自己训练
第一条要定下来的就是对话引擎。我的态度非常明确:不自己训练模型,也不自己部署开源大模型来跑推理,直接调用现成的大语言模型接口。你可以在任意提供这类服务的平台购买额度,然后拿到一个 API Key 就能用。选哪个服务的核心考量有三个:上下文长度是否够用、是否支持“工具调用”能力、每百万 token 的单价是否在你的预算范围内。
为什么优先看重“工具调用”?因为这决定了你的助手能不能真正做事。普通的对话模型只会给你回文字,哪怕它说得头头是道,也不会真的去帮你执行操作。支持工具调用的模型,会在回答里输出一个结构化的调用意图,比如“现在应该调用 get_weather 这个函数,参数是 city=上海”,你的代码拿到这个意图之后自己去执行函数,再把执行结果回填给模型,模型再组织语言回复你。这一整套能力如果没有,你后面所有自动化都无从谈起。
我在选型时还特意关注了流式输出能力。流式输出就是模型生成答案时一个字一个字地往外吐,而不是等全部生成完再一次性返回。实际体验中,流式输出的“打字机效果”会让人感觉助手反应特别快。虽然第一版用不上,但接网页界面时几乎必须得有,所以我把它列为必要项。
2.2 服务框架与数据存储
服务端框架我选了 FastAPI,原因很实际:它是异步框架,天然支持高并发和流式响应;它可以自动生成接口文档,调试时可以直接在浏览器里看到所有接口;它用 Python 写,跟整个 AI 生态的库对接最直接。你要是用得顺手,换成 Flask 或者其他任何轻量 Web 框架也完全可以,核心逻辑不受影响。但我个人建议没必要上重框架,这类助手的业务逻辑就一条线,轻框架反而更好维护。
数据存储方面,第一版我用的是 SQLite。别看不起这个单文件数据库,它零配置、单文件、支持事务,对个人项目来说足够稳定。对话历史、工具调用日志、用户配置全部放进去,还是一个文件,备份直接拷走就行。等以后数据量大了,再迁移到专门的数据库也不难,因为我在操作层做了一层隔离,存储引擎是可替换的。
向量数据库我是在第二版才引入的。要知道,普通数据库适合存结构化的记录,但如果你希望助手能“回忆”起很久之前聊过的一句相关内容,就需要向量检索。它能根据语义相似度把历史对话捞出来,塞进当前上下文里。第一版不用它,是因为核心流程都没跑稳,过早加上只会多一个失败点。
2.3 工具调用:让助手从“能聊”变成“能干”
工具调用是整个项目里最值得花心思设计的部分。我的设计思路是:每个工具就是一个普通的 Python 函数,函数上方用装饰器注册,声明它的名称、描述、参数结构。助手收到用户问题时,会先根据这些描述决定“我需要调用哪个工具”,然后生成一个 JSON 格式的调用指令,我的服务端拿到指令后解析参数、执行函数、把结果返回给助手。整个过程可以循环多次,比如助手先查日历发现下午有会,再调用提醒工具设置闹钟。
这里有一个关键设计:工具的描述必须写得足够清楚。很多人的工具调用失败,不是因为代码有 Bug,而是因为函数描述含糊,模型根本不知道这个工具什么时候该用、参数怎么填。我的做法是参照写 API 文档的标准去写工具描述,每个参数都标明类型、取值范围和示例。前期多花十分钟写描述,后面能省下大把调试时间。我第一个版本就写了五个工具:查天气、创建待办、删除待办、执行本地命令(白名单模式)、生成短链接。这些工具足够覆盖绝大多数日常需求,也足够验证整个机制是否可靠。
3. 从零开始搭建:完整实操过程
3.1 环境准备与依赖安装
我建议你在 Python 3.10 以上的版本上操作,因为更高版本的 Python 对类型注解和异步编程支持更好。先创建一个干净的虚拟环境,再把需要的依赖装进去。实际用到的核心包并不多:Web 框架用 fastapi,服务器用 uvicorn,网络请求用 httpx,数据库用 sqlite3(Python 自带),配置文件解析用 pyyaml,再加一个处理时间用的 python-dateutil。这些包都很稳定,不需要特别指定版本,直接装最新版就行。
装完依赖后,建议立刻验证一下环境,避免后面报错时分不清是环境问题还是代码问题。我当时在虚拟环境里先写了一个最简单的 FastAPI 接口,访问一下能看到返回 JSON,再开始正式的开发。这步只要两分钟,但它能帮你把变量范围缩小到“代码逻辑”本身。
3.2 先写一个能聊天的服务端
我先把对话流程写成了最简单的“请求-响应”模式。客户端发来一条消息,服务端把历史消息列表和当前问题拼在一起,发给大模型接口,拿到完整回复后再返回给客户端。核心逻辑不复杂,但有一点必须处理好:多轮对话不是简单把所有历史都丢给模型,因为模型上下文窗口是有限的,如果聊天记录太长,会既费钱又影响响应速度。我采用的做法是保存最近二十条对话,超过的部分截断掉,第一版够用了。
下面这个示例是服务端入口的核心代码。它定义了一个聊天接口,请求体里带一个 message 字段,服务端会把 session_id 对应的历史会话从数据库里读出来,加上用户新发的消息一起提交给模型,然后把模型回答存回去。写完之后我立刻用 curl 做了测试:连续问“我叫小刘”“我叫什么名字”,确认助手能通过历史记录记住对话里出现过的信息。
from fastapi import FastAPI from pydantic import BaseModel import openai # 实际操作中换成你接的服务端SDK即可 app = FastAPI() class ChatRequest(BaseModel): session_id: str message: str @app.post("/chat") async def chat(req: ChatRequest): messages = load_history(req.session_id) messages.append({"role": "user", "content": req.message}) response = await call_llm(messages) save_history(req.session_id, messages + [{"role": "assistant", "content": response}]) return {"reply": response}3.3 加入工具调用能力
能聊之后,第二步就是打通工具调用。这一步是让助手真正变得有用的关键节点。我对整个交互流程做了标准化处理:先向模型发送用户消息和工具定义列表,如果模型返回的响应里包含工具调用意图,我就执行对应的函数,再把函数结果作为一条工具消息返回给模型,让模型继续生成回复。如果模型没有要求调用工具,我就直接返回生成的文字。
一个非常容易踩坑的点是:工具调用的结果必须干净、准确。模型是根据工具返回值来组织语言回复用户的,如果你返回的是一大段错误堆栈,模型要么胡编乱造,要么把你的代码错误原封不动念给用户听。我的做法是每个工具函数内部都做异常处理,统一返回结构化的结果,例如{"success": true, "data": {...}}或者{"success": false, "message": "城市名不能为空"}。这样模型拿到结果后,能清晰地知道接下来该怎么组织语言。
下面是一个注册工具和创建待办的简化示例。工具定义是一个 JSON 列表,每个工具包含名称、描述、参数属性;真正的执行函数则按约定取参数并返回结果。模型不会直接执行你的函数,它只负责“告诉你要执行哪个函数”,执行权始终在你手里。
TOOLS = [ { "type": "function", "function": { "name": "create_todo", "description": "创建一条待办事项", "parameters": { "type": "object", "properties": { "title": {"type": "string", "description": "待办标题"}, "due": {"type": "string", "description": "截止时间,格式YYYY-MM-DD HH:MM"} }, "required": ["title"] } } } ] def run_tool(name: str, args: dict): if name == "create_todo": title = args["title"] save_todo(title, args.get("due")) return {"success": True, "data": {"title": title}} return {"success": False, "message": f"未知工具: {name}"}3.4 让助手记住上下文
记忆模块我单独设计了一个存储表:session_id、role、content、created_at。每次对话结束时,把新增的消息写入表里。下一次请求时,读取这个 session 最近二十条消息拼装上下文。因为 SQLite 读写足够快,这种方式在单用户场景下的体验非常好,几乎感觉不到延迟。
后来我又加了一个叫“记忆摘要”的机制。当会话超过二十条时,把最前面的旧消息送给模型,让它生成一段摘要,把摘要存成一条特殊的系统消息,替代那批被截断的原始记录。这种方式比直接粗暴丢弃旧消息效果好得多,助手依然能记得很久以前聊过的关键信息,但 token 消耗保持稳定。我第一次实现这个机制的时候特别兴奋,因为它真正解决了“越聊越忘”的问题。
4. 把助手接到真实场景里
4.1 终端命令行入口
服务端跑起来之后,我给自己做了第一个客户端:终端命令行入口。它做的事很简单,读取用户输入,调用 HTTP 接口,然后打印返回结果。但这个简单的入口让我第一次感受到“助手已经在身边了”的实感。我可以直接在终端里问它“今天有哪些待办”,它会调用我注册的查询工具,把数据库里的任务列表拉出来给我看。
终端入口虽然简陋,却非常适合测试和调试。因为命令行没有浏览器渲染、没有网络抖动,出问题能很快定位到服务端逻辑。而且你可以用管道把别的命令输出喂给它,比如把日志文件的错误信息贴进去,问它“根据这段日志,问题最可能出在哪里”。这算是我日常使用率最高的一个场景。命令行入口代码不多,但我强烈建议你做,它能帮你积累大量模型行为观察经验。
4.2 做一个网页聊天界面
网页界面我用了一个非常轻量的方案:后端返回流式响应,前端页面用原生的 fetch 接口逐段读取文本,实现打字机效果。这样不需要额外引入前端框架,一个 HTML 文件就能搞定。界面本身很简单,左边是聊天记录,底部是输入框,但配合流式输出,体验已经很像商业产品了。第一次看到字一个一个蹦出来的时候,你会觉得之前所有的配置成本都值了。
做网页端时最需要注意的是流式响应的格式。FastAPI 里我用的是StreamingResponse,逐块把增量文本推给前端。前端接收到之后追加到当前消息里。你不需要实现得很完美,只要保证在网络波动时不会断掉整段回复就行。第二版我加了断线重连机制,效果好了很多,但这块以后可以单独写一篇再展开。
4.3 挂上定时任务与提醒
真正让助手从“被问才会干活”变成“主动帮你干活”的,是接上定时任务系统。我用了系统自带的计划任务工具,每分钟触发一次 Python 脚本,脚本去数据库里查有没有到期需要提醒的事情,如果有,就调用助手的接口生成一条提醒文案,再通过你常用的消息推送渠道发给自己。虽然是“轮询”实现的,但体验上已经接近主动推送了。
这一步有个小技巧:定时任务脚本本身不直接调用大模型,而是走助手的 HTTP 接口。这样做的好处是,所有业务逻辑都集中在服务端,脚本只负责触发和接收结果。否则你在脚本里再写一遍提示词组装、历史记录管理,很快代码就散了。把“入口”和“能力”分开,是我建议所有做这类项目的人一开始就养成的习惯。
5. 我踩过的坑和排查方法
5.1 Token 消耗失控
第一次跑通完整流程时,我随手把过去一整天的聊天记录全部塞进上下文,结果一次请求的费用高得吓人。后来我意识到,token 计算公式其实很直观:输入的消息数量、每条消息的长度、工具定义的长度,三者都会显著影响最终费用。尤其是工具定义,每发一次请求它都会被完整发送一次,工具越多,基础开销越大。我之前注册了十几个工具,光工具定义相关的 token 就占了很大比例。
解决办法是分级管理。高频工具永远在列表里,低频工具只在该场景需要时临时加入。另外给上下文加一个硬性上限,超过就触发摘要压缩。我还加了一个简单的统计中间件,每次请求结束后记录用了多少输入 token、多少输出 token,定期去看报表。数据一出来,哪里该优化就一目了然了。
5.2 工具返回的 JSON 解析失败
这个问题出现频率相当高。模型在决定调用工具时,生成的参数偶尔会出现格式异常,比如缺了引号、多了一个逗号,或者字符串里混了换行符。我最初严格按 JSON 解析,经常报错。后来改成了容错解析:先尝试标准解析,失败后用简化版的清洗逻辑去掉多余字符再解析。确实还有极少数情况解析不出来,这时候就返回给模型一个“参数格式错误,请重新调用”,它一般会自我纠正。
这个容错思路其实值得推广到整个项目。跟大模型打交道,永远要有一个心智模型:它不是程序,不会严格按契约输出,它偶尔会“手滑”。你需要在外面包一层校验和恢复机制,而不是抱怨它为什么生成错了。
5.3 API 并发限制与超时
接网页端之后,我发现一个问题:如果用户连续发起多个请求,或者流式回复还没结束就又发一条新消息,服务端会因为 API 调用超时而报错。排查后确认不是代码逻辑问题,而是我接入的大模型服务对并发连接数有硬性限制。解决办法是加一个信号量,控制同时进行的 API 请求数量;客户端侧也做了处理,在上一轮回复未结束时禁止发送新消息。
另外,API 调用超时时间也很有讲究。如果设置太短,模型思考时间稍长就会被中断;设置太长,用户在网页端等太久又感觉很卡。我这边最后取了一个折中值,配合流式输出让用户能马上看到反馈,体验上基本没有等待感。这个值要根据你接的服务商情况调整,没有通解。
5.4 多轮对话越聊越傻
很多人在使用过程中会发现,助手刚开聊时回答质量不错,对话超过十几轮以后就开始“犯糊涂”,甚至忘了用户提过的关键信息。原因在于上下文里塞入了大量无关信息,重要信息反而被稀释了。我做了一个关键信息锚定机制:每轮对话结束后,用模型抽取“用户在这轮对话里表达的核心诉求”,单独存在一个记忆字段里,下一轮组装上下文时把锚定信息放在最前面。
这个改动效果是质的飞跃。助手不再像金鱼一样七秒记忆,而是能在一小时后的对话里依然记得“用户想找一份数据分析相关的工作”“用户习惯用 Python 写脚本”。后来我又把锚定机制扩展到了项目数据里,实现了对多用户、多会话的支持。这是整个项目里投入产出比最高的一项改进。
6. 下一步还能怎么折腾
6.1 加入语音输入输出
文本对话跑顺以后,我第一个想到的就是语音。本地录音、调用接口做语音识别,把转写出的文字送入助手,再把助手的文字回复用语音合成读出来,整个链路不难。难点在于麦克风唤醒和噪音处理,终端环境安静的时候效果很好,但一放到嘈杂环境就拉胯。如果你只需要“静音环境下的语音助手”,可以直接用这个方案;如果想要远场唤醒,就得换一套专门的硬件方案了。
6.2 接入本地知识库做 RAG
让助手回答你自己文档里的内容,是目前我觉得最实用也最酷的扩展。做法是把自己的笔记、报告、手册全部切成小段,做向量化之后存进向量数据库。用户提问时,先把问题转成向量,在库里检索出最相关的几段文本,再把这些文本连同问题一起交给模型,让它基于这些材料回答。我把它用在一个工作场景里:把一堆产品文档喂进去之后,想问什么直接问,不用自己翻文档了。但记得控制检索片段数量,否则上下文很快就被塞满。
6.3 把它变成团队协作用的机器人
OpenClaw 的能力全部通过 HTTP 接口暴露,这意味着你可以把它接入任何支持自定义机器人的协同工具里。我当时做了一个简单的适配层,让团队群里发消息就能触发助手,助手也能主动往群里推送提醒。这个场景下要考虑多用户会话隔离、消息频繁触发、权限控制等问题,但基础架构不需要改变。第一版只需要给每个会话生成一个独立 ID,存一套独立历史就行。
这类 AI 助手的边界其实是靠你一个个工具函数划出来的。你把工具写得越多、越好,它能干的事就越广。我始终觉得,技术本身不复杂,复杂的是你对场景的理解。每当你发现一个重复性工作,都可以想一想:能不能抽象成一个工具函数,交给 OpenClaw 去处理?它能帮你解决的,可能远超出你最初的预期。