
1. 为什么我要自己搓一个本地 CLI AgentClaude 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 侧装官方 SDKpip install ollama到这里本地推理链路就通了。但实际开发中你会发现本地 7B 模型在复杂工具编排、多轮推理上还是吃力有些任务需要更强的模型来兜底。这时候可以准备一条云端通道作为补充TaoToken 的 API 兼容 OpenAI 格式切换成本很低。它的接入信息如下Base URLhttps://taotoken.net/apiAPI 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/apiAPI Key 填控制台生成的Model ID 填你要用的模型名。Codex 的话对应的是~/.codex/auth.json结构类似把 base_url 和 api_key 填进去即可。需要说明的是本地 Ollama 和云端 API 不是二选一的关系。我的做法是本地模型跑日常轻量任务和隐私敏感的命令遇到需要强推理的复杂编排再切到云端。两套配置都留着用环境变量控制走哪条路。TaoToken 在这里的角色是提供一个稳定的 OpenAI 兼容入口省得为不同模型改代码。3. 可复制配置工具 schema 与调用循环这一节是核心我把工具注册和调用循环拆成可直接复制的片段。先定义工具 schema这是给模型看的「说明书」告诉它有个工具叫execute_shell_command接收一个字符串参数commandimport 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, shellTrue, capture_outputTrue, textTrue, timeout15 ) 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(modelmodel, messagesmessages, toolsTOOLS) 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(modelmodel, messagesmessages, toolsTOOLS) 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_outputTrue要配上不然输出会直接打到终端模型拿不到。6. 继续扩展与接入建议骨架跑通之后扩展方向其实很多。Shell 只是第一个工具你可以照着同样的 schema 格式再加read_file、write_file、query_database模型会自动判断该调哪个。工具描述写得越清楚模型选得越准。如果打算把这个 Agent 长期跑起来或者需要远程访问本地调试完迁到服务器是自然的选择。本地验证逻辑服务器负责常驻两边用同一套代码只改模型配置就行。需要更强模型兜底复杂任务时把本地ollama.chat换成 OpenAI 兼容调用Base URL 指向https://taotoken.net/apiKey 从控制台取模型 ID 按需选其余循环逻辑一行不用改。想先感受一下不同模型在工具调用上的表现差异可以直接在模型对话页面里试几轮对比本地 Qwen 和云端模型的判断准确度。长期做编码类 Agent 的话Coding Plan 那条线更适合持续跑任务。接入文档里有完整的参数说明和示例配置卡住的时候翻一翻比瞎试快。