☰
不用 Claude Code,不花一分钱!用 Python + Ollama 手搓本地 AI CLI Agent 的 Tool Calling 实战
2026/10/10 22:56:05 网站建设 项目流程

1. 为什么我要自己搓一个本地 CLI Agent

Claude Code 这类命令行 AI 助手确实好用,但用久了总会碰到几个绕不开的问题:模型跑在云端,源码和日志等于交出去了;API 按 token 计费,跑几个长任务账单就上来了;网络一抖,整个终端就卡在那里等响应。对于天天泡在 Terminal 里查日志、跑 Docker、翻 Git 历史的开发者来说,这些摩擦点会不断累积。

CLI Agent 的本质其实不神秘,拆开看就三块:一个大模型负责理解意图,一套 Tool Calling 机制负责把意图翻译成可执行动作,一组系统工具负责真正落地。Claude Code 强在模型能力和工程打磨,但这套骨架你自己也能搭。用 Python 加 Ollama,把本地 Qwen 模型接进来,再给它注册一个执行 Shell 命令的工具,一个能跑在自己电脑上的命令行 AI 助手就成型了。全程不联网调模型,不花一分钱 API 费用,数据不出本机。

这篇文章面向的是想搞懂 Agent 底层循环、又不想一上来就啃框架源码的开发者。我会从 Ollama 环境准备讲起,给出可复制的工具 schema 定义、完整的调用循环代码,然后实际跑一次「查看磁盘空间」的请求,把模型返回、命令执行、结果回填的每一步都摊开看。最后会集中排几个新手最容易踩的报错,比如 401、tool_calls 解析失败、模型不支持工具调用这些。

适合谁看:有基础 Python 能力,用过命令行,对 AI Agent 感兴趣但还没动手写过完整循环的人。不需要你有 GPU,普通笔记本跑 7B 级别的量化模型就够验证流程了。

2. 前置准备:Ollama 安装与 TaoToken 接入配置

先说本地模型这条线。Ollama 的安装很直接,去官网下载对应系统的安装包,装完在终端敲ollama --version能出版本号就成。然后拉一个支持 Tool Calling 的模型,Qwen 系列在这块支持得比较稳:

ollama pull qwen2.5:7b

拉完之后ollama list能看到模型就绪。Python 侧装官方 SDK:

pip install ollama

到这里本地推理链路就通了。但实际开发中你会发现,本地 7B 模型在复杂工具编排、多轮推理上还是吃力,有些任务需要更强的模型来兜底。这时候可以准备一条云端通道作为补充,TaoToken 的 API 兼容 OpenAI 格式,切换成本很低。它的接入信息如下:

  • Base URL:https://taotoken.net/api
  • API Key:在控制台创建,地址是 https://taotoken.net/console/api-keys
  • 模型 ID:按需选择,比如claude-sonnet-4-5这类支持工具调用的模型

如果你用的是 Claude Code 这类工具,配置方式是在 settings 里指定 Base URL 和 Key。以 Claude Code 的配置文件为例,路径通常在~/.claude/settings.json,写入:

{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "你的_API_Key" } }

如果你用的是 Cline 或 Roo Code 这类 VS Code 插件,在 MCP 或模型配置里填三件套:Base URL 填https://taotoken.net/api,API Key 填控制台生成的,Model ID 填你要用的模型名。Codex 的话对应的是~/.codex/auth.json,结构类似,把 base_url 和 api_key 填进去即可。

需要说明的是,本地 Ollama 和云端 API 不是二选一的关系。我的做法是本地模型跑日常轻量任务和隐私敏感的命令,遇到需要强推理的复杂编排再切到云端。两套配置都留着,用环境变量控制走哪条路。TaoToken 在这里的角色是提供一个稳定的 OpenAI 兼容入口,省得为不同模型改代码。

3. 可复制配置:工具 schema 与调用循环

这一节是核心,我把工具注册和调用循环拆成可直接复制的片段。先定义工具 schema,这是给模型看的「说明书」,告诉它有个工具叫execute_shell_command,接收一个字符串参数command:

import subprocess import ollama TOOLS = [ { "type": "function", "function": { "name": "execute_shell_command", "description": "在本地执行一条 shell 命令并返回标准输出。用于查看系统信息、文件、进程等。", "parameters": { "type": "object", "properties": { "command": { "type": "string", "description": "要执行的 shell 命令,例如 df -h 或 ls -la" } }, "required": ["command"] } } } ]

然后是工具的实际执行函数,加一层基础安全限制,禁止sudo和rm -rf这类高危命令:

BLOCKED = ["sudo", "rm -rf", "mkfs", "dd if=", ":(){"] def execute_shell_command(command: str) -> str: for bad in BLOCKED: if bad in command: return f"命令被安全策略拦截:{command}" try: result = subprocess.run( command, shell=True, capture_output=True, text=True, timeout=15 ) out = result.stdout.strip() or result.stderr.strip() return out[:2000] if out else "(无输出)" except subprocess.TimeoutExpired: return "命令执行超时(15秒)"

接下来是调用循环,这是整个 Agent 的心脏。逻辑是:把用户输入和工具定义一起发给模型,模型如果决定调用工具,就执行并把结果塞回消息列表,再发一次让模型总结:

def run_agent(user_input: str, model: str = "qwen2.5:7b"): messages = [ {"role": "system", "content": "你是一个本地 CLI 助手,需要系统信息时调用工具,不要凭空编造。"}, {"role": "user", "content": user_input} ] response = ollama.chat(model=model, messages=messages, tools=TOOLS) msg = response["message"] if msg.get("tool_calls"): for call in msg["tool_calls"]: fn = call["function"]["name"] args = call["function"]["arguments"] cmd = args.get("command") if isinstance(args, dict) else args print(f"[执行] {cmd}") result = execute_shell_command(cmd) messages.append(msg) messages.append({"role": "tool", "content": result}) final = ollama.chat(model=model, messages=messages, tools=TOOLS) return final["message"]["content"] return msg["content"]

注意arguments在不同 Ollama 版本里可能是 dict 也可能是 JSON 字符串,代码里做了兼容。这套配置直接存成agent.py就能跑。

4. 验证请求:一次真实的命令执行与结果回填

配置写好了,得实际跑一次才算数。在终端里执行:

python -c "from agent import run_agent; print(run_agent('帮我看看磁盘还剩多少空间'))"

预期会看到类似这样的输出。第一行是工具被触发的日志:

[执行] df -h

然后是模型拿到df -h结果后的总结,大概长这样:

根据 df -h 的输出,你的根分区 /dev/sda2 总容量 234G,已用 89G,剩余 133G,使用率 40%。/boot 分区剩余充足。目前没有分区接近写满,磁盘空间健康。

这个过程里发生了两次模型推理。第一次模型判断需要调用工具,生成了df -h这个命令;Python 执行后把输出回填到 messages;第二次模型读取命令结果,用自然语言总结。你可以把df -h换成top -bn1 | head -20试试 CPU 占用,或者git status看仓库状态,流程完全一样。

如果想验证多轮上下文,可以在同一个进程里连续调用。把 messages 提到函数外面维护,第二次问「里面最大的文件是哪个」,模型能记住上一轮进入的目录。这一步验证通过,说明你的本地 CLI Agent 骨架已经能干活了。

5. 常见报错排查:401、tool_calls 解析与模型不支持

跑不通的时候,问题基本集中在这几类。

报错一:ollama._types.ResponseError: model does not support tools

这是模型本身不支持 Tool Calling。不是所有 Ollama 模型都带这个能力,qwen2.5、llama3.1这些是支持的,但一些老模型或纯对话微调版不行。解决方法是换模型,ollama pull qwen2.5:7b重新拉一个确认支持的版本。判断方法很简单,跑一次ollama show qwen2.5:7b,看输出里有没有 tools 相关的能力标记。

报错二:KeyError: 'tool_calls'或解析arguments时报 JSON 错

不同 Ollama 版本返回结构有差异。有的版本arguments直接是 dict,有的是 JSON 字符串需要json.loads。稳妥写法是先判断类型:

import json args = call["function"]["arguments"] if isinstance(args, str): args = json.loads(args)

另外msg.get("tool_calls")用 get 而不是直接索引,避免没有工具调用时抛异常。

报错三:401 Unauthorized或local proxy failed

这个通常出现在你切到云端 API 的时候。401 说明 API Key 不对或没带上,检查ANTHROPIC_API_KEY或对应环境变量是否设置正确,Key 有没有多余空格。local proxy failed一般是 Base URL 写错了,确认填的是https://taotoken.net/api而不是别的路径。如果用的是 Claude Code,检查~/.claude/settings.json里的ANTHROPIC_BASE_URL字段拼写。OAuth 相关的报错则多半是认证流程没走完,重新在控制台生成一次 Key 再试。

报错四:reading 'choices'或返回结构对不上

这是把 OpenAI 格式的响应当成 Ollama 格式解析了。Ollama SDK 返回的是response["message"],而 OpenAI 兼容接口返回的是response["choices"][0]["message"]。两套链路别混用,本地走ollama.chat,云端走 OpenAI SDK,各自解析各自的字段。

报错五:命令执行卡死

subprocess.run一定要带timeout,否则遇到top这种交互式命令会一直挂着。代码里设了 15 秒,你可以按需调整。另外capture_output=True要配上,不然输出会直接打到终端,模型拿不到。

6. 继续扩展与接入建议

骨架跑通之后,扩展方向其实很多。Shell 只是第一个工具,你可以照着同样的 schema 格式再加read_file、write_file、query_database,模型会自动判断该调哪个。工具描述写得越清楚,模型选得越准。

如果打算把这个 Agent 长期跑起来,或者需要远程访问,本地调试完迁到服务器是自然的选择。本地验证逻辑,服务器负责常驻,两边用同一套代码,只改模型配置就行。需要更强模型兜底复杂任务时,把本地ollama.chat换成 OpenAI 兼容调用,Base URL 指向https://taotoken.net/api,Key 从控制台取,模型 ID 按需选,其余循环逻辑一行不用改。

想先感受一下不同模型在工具调用上的表现差异,可以直接在模型对话页面里试几轮,对比本地 Qwen 和云端模型的判断准确度。长期做编码类 Agent 的话,Coding Plan 那条线更适合持续跑任务。接入文档里有完整的参数说明和示例,配置卡住的时候翻一翻比瞎试快。

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

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

立即咨询