ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

本地部署AI Agent:OpenClaw接入Claude Sonnet 4.6实战

本地部署AI Agent:OpenClaw接入Claude Sonnet 4.6实战

最近在研究本地部署智能体执行框架,正好把一个叫 OpenClaw 的开源项目(社区习惯叫“龙虾”)从拉代码到接上 Claude Sonnet 4.6 模型完整跑通了一遍,顺手把里边的技术实现原理也拆了一遍。这篇文章不聊空话,就讲讲这个框架要实现什么、模型调用链路长什么样、参数怎么配、踩过哪些坑,以及怎么把它改造成一个能长期稳定跑的服务。

这个项目解决的核心问题其实很朴素:你有很多想法想自动化执行,但每次都要自己写脚本、调接口、处理返回结果,很烦。龙虾做的事是把“任务拆解、工具调度、模型对话、结果回填”这一整条链路串起来,你只需要把目标告诉它,它会自己调用注册好的工具去完成。而 Claude Sonnet 4.6 在这个链路里扮演的是“决策大脑”的角色,负责理解意图、生成步骤、输出结构化调用指令。

适合谁来参考这篇文章?如果你正在找一套能本地跑的 AI Agent 框架,或者想搞清楚大模型函数调用到底怎么和业务代码联动,再或者你手头已经有一个类似项目但总在工具调用解析上报错,这篇都值得你看完。

1. 整体设计思路与需求拆解

1.1 龙虾到底解决了什么问题

先还原一下场景。假设你要让 AI 自动帮你统计一个目录里的文件大小、找出最大的几个文件,并生成一份 Markdown 报告。最原始的做法是你自己写一段 Python 脚本,用 os.walk 扫目录、排序、写文件,每一步都写死。但需求一变,比如改成“统计最近三天修改过的文件”,你又得改脚本。龙虾这类框架的思路是把“业务逻辑”和“模型决策”分开:业务逻辑以工具函数的方式注册进去,模型负责根据用户输入的描述动态决定调用哪个工具、传什么参数。

这个设计的价值在于:同样是这组工具,你让模型完成“统计文件大小”它能做,你让它“找出最近的日志文件并打印尾部内容”它也能做。工具是固定的,组合方式是模型在运行时生成的,这就是 Agent 框架和普通脚本之间最本质的区别。Claude Sonnet 4.6 在这里承担的就是组合决策这件事,它的工具调用稳定性直接决定了整个框架好用不好用。

再往深一层说,Agent 框架把“能用代码写死的逻辑”和“需要模型现场发挥的逻辑”做了分离。前者是工具函数,后者是规划决策。这个分离让系统的可维护性提高了不少:你要增加一个新能力,不需要改模型提示词,只需要注册一个新工具;你要调整执行策略,不需要改工具函数,只需要调模型的系统提示和采样参数。

1.2 为什么选 Claude Sonnet 4.6 作为核心模型

在选择模型的时候,我对比过几个方向。首先是工具调用(Function Calling)的稳定性,简单说就是模型能不能稳定输出结构正确的 JSON。Sonnet 4.6 在工具调用场景里表现比较靠前,尤其是面对多工具候选时,很少出现参数名拼错、类型对不上这类低级问题。其次是代码解释能力和指令遵循能力,当用户描述不够精确时,模型能先提出澄清问题,而不是盲目开跑。第三是响应速度,本地 Agent 场景里每轮调用都有多轮工具执行,单次推理如果慢会导致整体体验很拖沓。

当然,这并不是说其他模型不能跑,只是在这个“以工具调用为主”的场景里,Sonnet 4.6 的性价比和稳定性比较均衡。框架本身做成了模型可插拔的结构,你后续想换成别的模型,只需要改配置里的模型标识和对应的消息协议适配器就可以,这部分的抽象在龙虾里做得还是相当克制的,没有为了支持所有模型而把核心逻辑搞成一团乱麻。

1.3 技术路线选型背后的取舍

我在搭这套东西之前,也考虑过两种完全不同的路线。一种是从零开始,自己写工具调度和消息循环;另一种是直接用龙虾这样的现成框架。自己写的好处是代码完全可控,坏处是坑非常多——上下文裁剪怎么做、工具结果怎么回填、并发执行时怎么跟踪状态,这些看起来是小问题,实际写起来全是细节。用现成框架的好处是这些基础设施已经有人趟过坑了,你只需要关注业务工具本身。

我自己选择用龙虾,还有一个原因是它对工具注册的抽象比较轻量。有些框架会把工具封装成一套复杂的类继承体系,写一个工具要理解一堆父类方法,学习成本很高。龙虾这边是用装饰器加函数定义,写起来和普通函数差不多,心智负担很小。对于想把主要精力放在业务逻辑上的人来说,这才是更舒服的姿势。

2. 运行时链路与核心模块拆解

2.1 一次完整任务请求是怎么流动的

我建议你在接触任何 Agent 框架时,先画出一条调用链路出来,后面所有配置和排查都围绕这条链路展开。龙虾的核心链路大致是这样:

  1. 用户输入一条自然语言指令。
  2. 调度器把指令和当前可用的工具清单组装进消息上下文。
  3. 发送给 Claude Sonnet 4.6,模型返回两种可能之一:直接回答,或者返回一批工具调用指令。
  4. 如果是工具调用指令,执行器去注册表里找到对应函数,校验参数后执行。
  5. 执行结果以工具消息回填进上下文,再次发给模型。
  6. 模型根据执行结果决定继续调用还是给出最终答复。
  7. 最终答复通过输出层返回给用户。

这里最需要注意的是第 4 步和第 5 步的回环设计。龙虾没有把工具执行结果只是简单拼到对话里,而是把它单独放在工具角色消息中,每条消息都带工具调用 ID。这样模型能够精确对应“哪次调用失败了”“哪个参数传错了”,在多工具并行执行的场景下尤其重要。我自己在第一版实现时偷懒,把所有工具结果塞进一条用户消息里发给模型,结果模型经常搞不清楚返回值属于哪个调用,后来改成保持角色结构就稳定多了。

2.2 消息角色与上下文管理机制

聊到消息结构,就顺便说说龙虾是怎么管理上下文的。和直接调用大模型 API 不同,Agent 框架要维护的不只是用户和助手之间的对话,还有大量工具产生的中间结果。如果每一轮工具执行结果都累积在上下文里,不用多少轮就会撑爆窗口。龙虾默认的策略是滑动窗口:保留最近的 N 轮关键消息,早期消息会被裁掉或者压缩成摘要。我建议你在生产中把 N 设在 20 到 30 之间,太小会让模型丢失任务开头的重要要求,太大则浪费 token。

另外要特别强调系统提示词(system prompt)的组合方式。龙虾会把三部分拼进系统提示:项目自身的执行规则、用户自定义的偏好、以及当前时刻可用的工具清单。工具清单不是静态写死的,而是每轮请求时动态序列化。为什么要这样设计?因为一旦工具数量超过几十个,全量塞进去会显著挤占上下文空间,模型也容易在选工具时走神。动态裁剪后,模型每次看到的是一份精简且和任务强相关的工具列表,工具选择的准确率会明显上升。

2.3 工具注册表与参数校验

工具层是龙虾项目改动成本最高的地方,也是最应该提前设计好的地方。每个工具函数在注册时至少要声明三样东西:函数名、描述、输入参数的 JSON Schema。函数名要短且语义唯一,描述要写清楚“什么场景下用这个函数”“不要什么时候用它”,JSON Schema 要严格声明类型、必填项和各字段含义。

我踩过一个比较典型的坑:给一个“查询订单”的工具写描述时只写了“查询订单状态”,结果模型经常把“查询用户列表”这种需求也路由到这个工具上。改成“仅在用户提供订单编号时使用,用于查询订单当前处理状态;若用户想要查询用户资料,请使用 lookup_user”之后,路由准确率立刻上来了。这里的关键原因是模型做工具选择时依赖的是文本语义匹配,描述里的边界越清晰,匹配越准。

参数校验建议放在执行器层统一做,不要让业务函数自己承接防御逻辑。龙虾的做法是,在执行前先按注册时声明的 Schema 做一次 JSON 校验,不通过就直接生成一条“参数校验失败”的工具返回消息,回填给模型让它重新调用。这个设计让业务函数保持干净,也让模型能拿到结构化错误信息去自我纠正,而不是面对一段堆栈日志发呆。

3. 实操要点与具体配置

3.1 环境准备与依赖安装

我自己是在一台 Linux 服务器上跑的,系统是常见发行版,Python 版本用了 3.11,Node 环境也装了一份,因为龙虾的某个辅助模块依赖 Node。建议你至少准备 Python 3.10 以上环境,太老的版本在新语法和类型注解上会有兼容问题。

拉取项目之后,先看仓库里的依赖清单文件。安装依赖时用虚拟环境,不要直接怼到系统 Python 里:

git clone <项目仓库地址> openclaw cd openclaw python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt

如果你遇到依赖装到一半报编译错误,多半是某些原生依赖缺少系统库,比如编译 JSON 解析相关的扩展时需要 libffi-dev,这类问题在常见发行版里用包管理器装一下即可。我不建议在生产环境里省略虚拟环境这步,因为后面配置模型 SDK、异步框架时依赖冲突的概率并不低。另外,如果你用的是 Windows,建议优先考虑 WSL 环境,有些异步文件监控模块在原生 Windows 下的表现确实不如在 Linux 下稳定。

3.2 模型接入配置与密钥管理

模型接入的核心配置文件通常是一个 YAML 或 JSON 文件。我这里用 YAML 示例,主要字段如下:

model: provider: claude name: claude-sonnet-4-6 base_url: ${CLAUDE_API_URL} api_key_env: CLAUDE_API_KEY temperature: 0.2 max_tokens: 4096 timeout_seconds: 120 scheduler: strategy: sliding_window keep_recent_messages: 25 toolbox: auto_discover: true strict_schema: true

这里有几个容易忽略的点。api_key_env指向一个环境变量名而不是直接把密钥写在文件里,就是为了避免配置文件被提交后泄露密钥。base_url不要写死,用${CLAUDE_API_URL}这种环境变量占位符,这样以后切换不同的接入端点时只改环境变量即可。temperature我建议设置在 0 到 0.3 之间,工具调用场景需要的是确定性,而不是创意发挥;设高了以后,模型可能在生成 JSON 参数时偶尔冒出多余的注释文字,导致解析失败。

使用前把环境变量准备好,可以放在.env文件里,龙虾启动时会读取:

export CLAUDE_API_KEY="sk-xxxxx" export CLAUDE_API_URL="https://api.example.com/v1/messages"

注意.env文件不要提交到版本管理里,建议把它写进.gitignore。密钥如果已经泄露了,赶紧去控制台吊销重建,别抱着侥幸心理。我还会在配置里额外加一个request_retry字段,默认值是 3 次重试,这个在模型服务偶尔抖动时相当管用。

3.3 写一个最小可跑通的调用示例

在跑通整个框架之前,我强烈建议先用最裸的 HTTP 请求验证一把模型接口通不通。这样能帮你区分“模型接口的问题”和“框架自身的问题”。下面是我用的最小验证脚本:

import os import requests from dotenv import load_dotenv load_dotenv() API_URL = os.getenv("CLAUDE_API_URL") API_KEY = os.getenv("CLAUDE_API_KEY") MODEL_NAME = os.getenv("CLAUDE_MODEL_NAME", "claude-sonnet-4-6") payload = { "model": MODEL_NAME, "max_tokens": 1024, "messages": [ {"role": "user", "content": "请把下面这句话压缩成十个字以内:今天天气真的非常好,适合出门散步。"} ], } resp = requests.post( API_URL, headers={ "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", }, json=payload, timeout=60, ) if resp.status_code == 200: data = resp.json() print(data["choices"][0]["message"]["content"]) else: print("Request failed:", resp.status_code, resp.text)

跑通这一步之后,你至少可以确认三件事:密钥有效、API 端点可访问、模型名称写得对。很多人在这一步就发现 401 或 404,省下了后面折腾框架的时间。我再多一句嘴,max_tokens不要给太小,尤其当后续接上工具调用后,模型可能需要生成一大段 JSON 结构,1024 只是验证用途,正式跑任务我至少给到 4096。

3.4 对接龙虾执行器并注册一个业务工具

接下来把验证好的模型调用接进龙虾。第一步是建一个工具模块,比如tools/file_tools.py,在里面注册一个“统计目录大小”的工具:

from openclaw import tool from pathlib import Path @tool( name="sum_dir_size", description="统计指定目录下所有文件的总大小,单位字节。仅当用户需要了解目录占用空间时使用。", input_schema={ "type": "object", "properties": { "path": {"type": "string", "description": "要统计的目录绝对路径"} }, "required": ["path"] } ) def sum_dir_size(path: str) -> dict: total = 0 for p in Path(path).rglob("*"): if p.is_file(): total += p.stat().st_size return {"total_bytes": total}

注册完之后,在龙虾的启动配置里通过toolbox.auto_discover或手动 import 的方式加载这个模块,然后启动服务,给模型发一条“帮我看看 /data 目录总共占了多少空间”。观察日志,你会看到大致的执行过程:模型先返回一个sum_dir_size的工具调用,执行器定位函数并传参/data,函数执行返回字节数,模型基于返回值生成最终回复。

这个流程跑通后,你等于打通了“自然语言指令到业务代码”的整条链路。剩下的所有工作都可以基于这套模式扩展:多注册几个工具,写更复杂的业务流程编排。

4. 安装配置后的功能延伸与参数调优

4.1 工具描述与提示词模板的优化顺序

很多人在框架跑通之后第一反应是“多塞点工具进去”,但实际体感是工具一多,模型选择就开始飘了。我自己的优化顺序是:先改工具描述,再精简动态工具列表,最后才是调 temperature 之类的采样参数。工具描述里的关键词要和用户请求里的常见说法对齐,比如用户习惯说“看一下这个文件夹多大”,那描述里最好也出现“文件夹占用”“目录大小”这类近似表达。

提示词模板方面,龙虾默认模板已经比较可用,但建议根据你的业务场景微调系统提示里“工作原则”这一段。比如我希望模型在执行多步任务时每步都以“我先调用 XX 工具确认 XX”开头,就可以在模板里加一句:每执行一步工具调用前,用一句话说明打算调用的工具和目的。这能让最终报告更容易读懂,也能让模型在步骤间保持逻辑连贯性。

4.2 温度、重试与并发的平衡

把配置从开发环境搬到生产之前,建议先做一轮压测,不然你可能毫无心理准备地撞上速率限制。模型服务方通常会对每分钟请求数或 token 数做限制,一旦超限会返回 429。龙虾内置了指数退避重试机制,默认从 1 秒开始,每次翻倍,最多重试 5 次。我在实际跑任务时把单任务并发控制在 3 到 5 个请求以内,既能把吞吐提上去,又不会频繁触发限流。

temperature 的取值有点讲究。纯对话场景你或许会调到 0.7 以上让人物更有“灵魂”,但工具调用场景我建议固定为 0。初学者容易忽略的是,即便 temperature 为 0,模型输出也不是完全确定性的,因为推理过程本身有随机性。所以不要因为一次输出差异就怀疑是配置问题,多看几次结果再下结论。

还有一个容易被忽视的参数是max_tokens。工具调用返回的 JSON 不长,但有些模型在生成过程中会先输出思考过程再输出工具调用,思考过程也会占用 token 额度。如果 max_tokens 卡得太死,模型可能在工具调用生成到一半就被截断,导致 JSON 不完整。我一般给到 4096 或更高,宁可多留余量,也不要因为这种低级截断问题重启任务。

4.3 将龙虾接入外部应用的方式

龙虾跑起来以后,你可以用几种方式对外提供能力。最简单的是直接使用命令行交互模式;稍微正式一点的是把龙虾的核心执行器封装成 Python 函数,在自己的 Web 服务里调用;再进一步可以直接启用内置的 API 服务模式,监听本地端口,让其他服务通过 HTTP JSON 请求来提交任务。

我倾向于用第三种方式,因为解耦性好。外部系统不用关心龙虾内部怎么调度工具,它们只需要 POST 一个{ "task": "..." },异步等待回调结果就行。需要注意,如果对外暴露服务,一定要做鉴权,至少加一个简单的 token 头校验,否则本地端口一旦被局域网内其他设备扫描到,就可能被拿来消耗你的模型额度。我在这个服务前面还加了一个简单的请求日志中间件,记录每次请求的来源 IP、任务描述和耗时,方便后续排查问题。

5. 常见问题与排查实录

5.1 鉴权失败与 401 问题

出现 401 时,先检查环境变量是否真的传进了进程。很多时候是.env文件没被加载,或者你在 shell 里 export 之后又改了文件但没重新加载。第二常见的原因是密钥字符串带了不可见字符,复制粘贴时尤其容易出问题。我的习惯是在终端里用echo "${#CLAUDE_API_KEY}"打印密钥长度,跟控制台显示的位数对比一下,差太多就说明复制出错。还有一种情况是配置里 base_url 指向的接入点与密钥不匹配,这个属于配置错误,仔细核对一下就好。

5.2 模型返回内容解析失败

如果你发现工具调用链路偶尔会断,多半是模型生成的参数 JSON 不符合注册 Schema。排查思路是打开龙虾的 debug 日志,找到模型返回的原始内容,直接肉眼扫一遍。常见现象包括:参数名被模型改写成了相近词、字符串类型被传成了数字、多了一个无关字段。对应解法是压缩工具的描述长度、降低 temperature、把 Schema 里每个字段的说明写得更具体。千万别一上来就改代码去“兼容”模型的错误输出,那样只会越补越乱。

如果模型频繁返回“这个任务我没有合适的工具”,不要急着怪模型,先看看当前工具清单里有没有和用户意图匹配的描述。工具描述写得太技术化,模型理解不了;写得太模糊,模型又容易选错。比较好的状态是描述包含三层信息:触发条件、参数含义、不适用场景。三层都说清楚,模型的选择准确率会好很多。

5.3 上下文超限或任务中途“失忆”

任务执行到一半,模型突然忘了最初的目标,这种问题我遇到过好几次。最直接的原因是滑动窗口把早期关键信息裁掉了。解决办法有两个:在系统提示里维护一份“任务不变式”,也就是无论如何都不可违背的核心要求;另一个是把用户原始任务单独作为一条固定消息保留在最新上下文中,每次回填时都带上。龙虾没有默认开启后者,我建议你根据自己的业务手动叠加一层。

还有一种“失忆”情况不是上下文问题,而是模型在执行大量工具调用后,把用户原始意图和工具返回结果混在一起了。这时我会在最终汇总前,让模型先复述一遍原始任务和已完成的步骤,再生成结论。这个“复述”动作能显著减少模型答非所问的概率,成本只是多一次调用,但效果很值。

5.4 速率限制与超时错误速查表

错误现象可能原因处理动作
401 Unauthorized密钥无效、环境变量未加载核对密钥、检查 env 加载顺序
404 Not FoundAPI 路径错误或模型名缺失修正 base_url、核对模型标识
429 Too Many Requests并发过高、触发速率限制降低并发、开启退避重试
500 Server Error服务端临时异常保留请求上下文,稍后重试
工具结果回填后模型反复调用同一工具工具返回信息不足以让模型判断丰富返回值结构,加入状态字段

6. 生产化改造与经验总结

我真的不建议只满足于本地跑通。把龙虾用于实际业务前,至少还要做三件事:日志链路追踪、任务状态持久化、关键操作人工确认。日志上,为每次任务分配一个唯一 ID,所有工具调用都带上这个 ID,排查问题时能省几个小时。持久化上,把任务状态和中间结果写入本地存储,这样服务重启后还能恢复未完成任务。人工确认方面,对于删除、清空、覆盖这类高影响工具,一定要在执行前加一道确认门槛,让模型先输出“即将执行 XX,影响范围是 XX,是否继续”,等你确认后再真正执行。

从模型策略上说,我个人的体会是:不要把所有希望都押在单次调用上。复杂的任务尽量由模型拆成多个小步骤,每步都基于上一步的结果做判断,而不是让它一步到位生成全部结果。这么做虽然会多一些调用次数,但整体稳定性显著上升。

最后分享一个小技巧:如果你经常遇到工具调用字段填错,试着在每次调用前给模型把关键参数用伪代码示例展示一遍,而不是只给 Schema。模型对“示例”的遵循能力往往比对“抽象描述”的遵循能力强得多。这个办法在我自己的一系列工具接入中基本是立竿见影的。折腾 Agent 框架这件事,最怕的是在错误的方向上不断优化,先把链路和数据结构理清楚,后面的一切都会顺很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进