终端AI编码agent实战:权限边界、AGENTS.md与工具调用循环

发布时间:2026/10/23 7:13:40
终端AI编码agent实战:权限边界、AGENTS.md与工具调用循环
终端里敲下 opencode 回车屏幕分成几块左边是不断往上滚的会话流右上是这一轮改动的文件清单右下角挂着我当前仓库的诊断信息。这个画面我用了大半年现在回头看从编辑器插件切到终端 agent最大的变化不是界面而是信任边界变清楚了——它跑在哪个目录、能碰哪些文件、执行哪条命令必须先问我全写在配置文件里能翻、能改、能版本化。opencode 是一个跑在终端里的开源编码 agentTUI 交互可接多家模型服务能读代码、改文件、执行命令、调用外部工具。它解决的问题很具体把“理解项目”和“动手改”塞进同一个进程省掉在聊天窗口和编辑器之间来回搬代码的力气。下面写的是我实际用下来的配置思路、踩过的坑以及固定下来的几条操作习惯适合已经在命令行里干活、想把 agent 流程做扎实的人完全没碰过终端 agent 的也能跟着走一遍。1. 为什么我把 AI 编程的入口挪回了终端1.1 编辑器插件卡在“上下文搬运”这一段早期我用的是编辑器里的对话面板流程大概是选中一段函数复制粘到侧边栏描述需求等回复再手动对照着把代码贴回去。单个小函数还行一旦涉及跨文件改动就开始难受——它看不到调用方我得把三个文件的关键片段一个个贴过去贴多了超出上下文窗口贴少了它就开始猜。最要命的是改完之后没有自动验证编译错没错、类型对不对、测试过没过全靠我自己再跑一遍。终端 agent 的差别在于它直接站在工作区里。它读文件不需要我搬运跑命令不需要我转述输出改完的差异会以文件清单的形式摆出来给我过目。这个链路短了不止一环。我印象最深的一次是给一个跨平台项目改配置加载逻辑涉及四个模块的导入路径。以前在插件里做这件事我前后贴了二十多段代码换成终端 agent 之后我只说了一句“把配置加载统一到 src/config 下保留旧路径的兼容导出”它自己 grep 出所有引用点列了个清单让我确认然后逐个改。我做的事只剩审阅。1.2 它本质是个“工具调用循环”不是聊天框理解这一点后面所有配置决策就都顺了。模型的输出不只是文本还可能是结构化的工具调用请求读某个文件、搜某个关键词、执行某条命令、写某个路径。本地运行时接到请求后执行把结果回填给模型模型再决定下一步。这个循环一直转到模型认为任务完成或者撞到权限墙。所以真正决定它好不好用的不是界面多漂亮而是三件事它能调用哪些工具、每次调用要不要经过我、工具返回的信息够不够它判断。这三件事全在配置文件里。界面只是壳。也正因为是循环成本是滚雪球式的。一次读十个文件、跑五次 grep、再改三处代码token 消耗跟对话轮次不是线性关系。我在第一个月就吃过这个亏一个“帮我看看这里为什么报错”的模糊提问它连着读了十几个文件最后给出的结论是我早就知道的那句“变量没初始化”。问题不在它笨在我没说清楚我排到哪一步了。1.3 三类人适合两类人先别折腾用得顺手的画像大概是这样日常在终端里跑命令对 git diff 不陌生手上同时维护两三个仓库需要来回切或者想把 agent 塞进脚本和流水线。这类人上手成本几乎为零配置文件的语法看一眼就懂。不太适合的是两类。一类是严重依赖图形化 diff 的逐行高亮、点选暂存这些操作在终端里确实没有替代品虽然有外部编辑器可以调起来但体验是断的。另一类是刚学编程、还没建立起“什么算改坏了”的判断力的agent 一次改八个文件你没法评估它对不对最后只能全部回滚反而浪费更多时间。我的建议是先用它做只读的事情解释代码、找调用链、写注释、生成测试草稿等你能快速判断它输出质量了再放开写权限。2. 从安装到第一次对话中间隔着三个容易漏的前置条件2.1 三条安装路径我按“能不能自动升级”来选常见的安装方式有三种我都在不同机器上试过# 方式一官方安装脚本装到用户目录不碰系统包管理 curl -fsSL 安装脚本地址 | bash # 方式二语言包管理器适合已经装好运行时的人 npm i -g opencode-ai # 方式三系统包管理器 brew install opencode选哪个我的判断标准很土哪条路径能一条命令升级就用哪条。这种工具迭代快配置字段、命令参数、工具名小版本之间都可能变。安装脚本装出来的直接opencode upgrade就能升包管理器装的就跟着包管理器走。我踩过一次坑是在一台老机器上用系统包管理器装版本落后了两个小版本配置里新加的字段被静默忽略我对着文档排查了半小时才发现是版本问题。从那以后我养成一个习惯出任何怪事先跑opencode --version跟文档对一遍。提示不要在同一条机器上混用多种安装方式。PATH 里出现两个同名可执行文件时你调用的可能不是你以为的那个。2.2 终端能力和运行时版本比想象中更容易出问题这个工具是 TUI对终端有基本要求真彩色支持、UTF-8、能正确处理宽字符。三个地方最容易翻车。第一是tmux 或 screen 里的 TERM 变量。有些环境下 TERM 被设成screen而不是screen-256color颜色会掉边框会错位按键偶尔失灵。我现在的做法是在 tmux 配置里显式写死set -g default-terminal screen-256color省得每次进会话都要手动 export。第二是字体和 Unicode。界面里用了不少制表符和状态图标字体缺字时会显示成方块。这个不算功能问题但会严重影响你读状态——尤其是那个表示“文件已修改”的标记显示不出来的时候。第三是依赖的外部命令。搜索文件用的是什么、跑代码检查靠什么这些如果系统里没有功能会静默降级不报错但效果打折。我的检查清单是启动后先让它做一次“在这个仓库里找我刚才改过的那几个文件”看它搜得准不准不准就说明搜索这块有问题。2.3 密钥别落在项目目录里接入模型服务需要凭证。工具提供了交互式登录命令opencode auth login它会把凭证存到用户目录下Linux/macOS 一般在~/.local/share/opencode/auth.json不在项目里。这一点很重要。我见过有人在项目根目录建了个.env存密钥然后git add .一把梭虽然及时发现了但那是运气好。如果你的团队有统一的密钥管理也可以用环境变量注入配置文件里引用变量名而不是明文。代价是每次开新 shell 都要保证变量存在忘了就是一堆 401。我自己的折中是个人机器走交互式登录CI 里走环境变量两套路径分开互不干扰。配置完可以用下面这条命令确认当前接的是哪个模型opencode models它会列出可选的模型清单。第一次跑的时候我盯着这个列表看了半天才明白能选多少取决于你配了几个 provider——不是所有服务都默认可用。3. opencode.json把“它能干什么”提前写死3.1 全局配置和项目配置是合并关系不是覆盖配置文件有两层用户级的~/.config/opencode/opencode.json项目根目录的opencode.json。我一开始以为是项目配置整个替换全局配置结果发现是按字段合并的。这个差异很关键。举个我实际遇到的场景全局配置里我设了默认模型和一个比较宽松的 bash 权限因为个人项目随便造某个客户项目里我只想收紧权限就在项目配置里只写了permission段。合并之后的结果是模型沿用全局的权限用项目的。这符合预期。但反过来如果我在项目里只写了model段那全局的权限规则会原封不动带过来——宽松的那套就跟着进了敏感项目。发现这个之后我在所有正式项目的配置里都把permission段完整写一遍不依赖继承。一个小技巧配置文件里加上 schema 引用字段编辑器就有一整路的字段补全和类型提示能挡掉大量因为拼错字段名导致的静默失效。3.2 provider 与 model 字段错一个字符就是 404模型接入这块配置形态大致是这样的字段名以你手上版本为准我这里写的是本地跑通的形态{ $schema: 官方 schema 地址, provider: { my-endpoint: { npm: 对应 SDK 包名, options: { baseURL: https://你的服务地址/v1, apiKey: {env:MY_API_KEY} }, models: { my-model-id: { name: 主模型 } } } }, model: my-endpoint/my-model-id }最容易出错的三个地方我按踩坑频率排序模型 ID 拼写。my-model-id里的连字符、大小写、版本后缀错一个字符就是 404而且报错信息通常只说“模型不存在”不告诉你有哪几个可用。我现在的做法是先opencode models看列表复制粘贴绝不手打。baseURL 的路径后缀。有的服务要/v1有的不要多一个斜杠少一个斜杠结果都不一样。判断方法很直接出 404 就先换baseURL别去怀疑密钥。密钥引用语法。用环境变量引用时写错格式密钥会被当成字面量传出去然后你收到一个 401还以为是密钥过期。注意options里的字段会被原样透传给底层 SDK工具本身不做校验。写错了不会在启动时提醒你只会在第一次请求时以奇怪的错误形式冒出来。3.3 权限规则我给自己定的三条硬线permission段是我花时间最多的地方因为它决定了 agent 的“手”能伸多长。我的规则是三条第一条写文件默认放行但删除类命令一律拒绝。理由很简单改错了能git checkout删了不一定能找回来。第二条涉及远端的命令必须确认。推送、发布、动线上环境这类操作一旦触发就没有本地回滚这回事。第三条通配符只放在最后。配置里的匹配通常是从具体到宽泛我把*放最下面兜底上面列出所有希望自动放行的只读命令比如查看状态、看日志、跑类型检查。{ permission: { edit: allow, bash: { git status: allow, git diff*: allow, npm run lint: allow, git push*: ask, rm -rf*: deny, *: ask } } }这套规则跑下来实际被拦住的次数不多但每次拦住都是真该拦的。有一次它想跑一条递归删除构建产物的命令被我的deny规则挡住了然后它改成了单独删指定目录——这就是我想要的行为不是禁止它做事是逼它换一条更保守的路。3.4 键位和主题只改两处别折腾默认键位基本够用我只改了两处。一是把切换模式的键换成我手指更顺的位置因为我在一次任务里可能要来回切十几次二是把打开外部编辑器的快捷键调出来写长 prompt 的时候终端内联输入框的体验实在太差了一行行挤着写思路都被打断。主题这块我试过几个配色最后的结论是对比度优先于好看。终端里同时有会话文本、文件清单、诊断信息三种不同性质的内容配色区分度不够的时候你会花时间分辨哪块是哪块。我现在用的是偏暗底色加高饱和强调色的组合看久了眼睛不累。4. AGENTS.md 的质量决定它是“懂项目”还是“瞎猜”4.1 /init 生成的初稿通常缺三块东西第一次进项目跑一下/init它会扫一遍仓库结构生成一份AGENTS.md——这是喂给模型的常驻项目说明。生成的初稿一般能覆盖技术栈和目录结构但缺的东西很固定构建和验证的确切命令。它可能写了“用 npm 构建”但实际项目里得跑npm run build:prod才有意义开发构建根本不走类型检查。不能碰的目录。生成产物、第三方 vendored 代码、数据库迁移脚本这些改了就出事的地方初稿里通常不提。代码风格里那些不成文的约定。比如“所有对外接口必须有显式返回类型”“错误统一走某个包装函数”这些从代码里能推断但很费劲写进说明里能省一堆来回。4.2 我固定写进去的四类内容现在我每个项目的AGENTS.md都有一个稳定骨架四类信息第一类验收标准。我会明确写改完之后必须跑哪几条命令全部通过才算完成。这条最值钱。以前它改完就说“已完成”我一看类型检查挂了三个错。写进说明之后它自己会跑一遍再回复。第二类模块边界。哪些目录属于哪个子系统跨模块调用要走哪层。这个写清楚了它就不会把业务逻辑塞进工具函数里。第三类禁止项。不新建抽象层、不改公共接口签名、不动迁移文件。这三条是我被坑出来的——它特别喜欢“顺手帮你抽个公共函数”抽完一编译五个调用方全炸。第四类常用命令的完整形态包括参数。别写“跑测试”写清楚完整命令和常用参数。## 验证 改完代码后依次执行 1. npm run typecheck —— 必须零错误 2. npm run lint -- --max-warnings 0 —— 不允许新增警告 3. npm test -- --runInBand —— 相关用例必须通过 ## 边界 - src/core/ 不依赖 src/features/ - 新增依赖前先确认 package.json 里没有等价能力的包 - migrations/ 目录只读不生成任何迁移4.3 反面案例写成了 README 的复制品我最早犯的错是把AGENTS.md写成了给新人看的项目介绍背景、架构图、模块职责写了两千字。结果效果很差。原因很实在——模型的注意力不是无限的。你写进去的每一条都在占预算而“这个项目诞生于哪一年”这种信息对它做决策零帮助。后来我做了一次裁剪从两千字压到四百字只留下“做什么、怎么验证、不许做什么”三类。效果立竿见影它开始稳定地跑验证命令也不再乱抽公共函数了。这个经验我觉得挺通用给 agent 的说明指令性内容优先于描述性内容。描述是给它建立背景指令是给它划边界后者永远更值钱。5. plan 与 build 两副面孔什么时候切5.1 先只读、后动手这条流程值得强制执行工具内置了两种模式一个是只读的规划模式一个是可写的执行模式界面上有快捷键来回切。我现在的固定流程是任何涉及三个以上文件的任务先在只读模式里让它出方案。为什么这么较真因为可写模式下它的默认行为是“边想边改”。一个判断错了文件已经改了你得回滚再重来。只读模式下它只能读和搜产出是一份改动清单改哪几个文件、每个文件改什么、为什么这么改。我审这份清单的成本远低于审一堆实际改动。有一次改一个跨平台项目的路径处理它给的方案里要动五个文件。我看清单的时候发现其中一个文件是自动生成的不应该手改。如果直接进执行模式它大概率会改那个文件然后下一次生成时冲突。这个拦截就发生在清单阶段。5.2 会话、压缩与回滚的配合会话管理这块有三个操作我几乎每天用/compact是压缩当前会话的上下文。什么时候用我的判断标准是当它开始重复问我已经回答过的问题。这是上下文被挤掉的信号。压缩之后它会保留要点、丢掉细节能续命但不是无损的——压缩前的那些细节它就真不记得了。/undo和/redo是回滚会话和改动。我用得最多的是在它改歪了的时候先回滚会话再回滚文件两步都做否则会话里还留着“我改了 X”的错误记忆下一轮它会基于这个错误前提继续推理。/editor是调外部编辑器写长 prompt。前面提过终端内联输入框写超过三行的需求就开始难受了。我现在的习惯是复杂需求先在编辑器里写清楚再提交。5.3 一次跨模块改造的完整复盘拿一个虚构的场景说某个模拟项目要把散落在各处的配置读取统一到单一入口。我的操作顺序是这样的。第一轮只读模式让它列出所有读取配置的位置产出清单。它给了十一个位置我扫了一遍划掉两个那是测试里的 fixture不该动。第二轮还是只读模式让它对剩下九个出改动方案重点是“如何保证旧调用方不炸”。它提议加一层兼容导出。第三轮切到可写模式让它按文件逐个改每改完一个我就看一次 diff。这里我特意没让它一次全改——一次改九个文件出问题你根本定位不到是哪一步引入的。改完跑验证类型检查通过两个用例挂了。我把报错原文完整贴回去这里有个技巧下面会讲它定位到是一个默认值的处理顺序反了。修完再跑全绿。整个过程大概四十分钟其中我审阅的时间占了一半以上。这个比例我认为是健康的——agent 省的是打字时间不是判断时间。6. 把重复动作沉淀下来MCP、自定义 agent 与无头模式6.1 MCP 的本地和远程两种接法MCP 是一套让模型调用外部能力的协议。配置起来分两类{ mcp: { local-tool: { type: local, command: [npx, -y, 某个 MCP 服务包], enabled: true }, remote-docs: { type: remote, url: https://服务地址/mcp, enabled: true, headers: { Authorization: Bearer {env:DOCS_TOKEN} } } } }本地型的启动一个子进程通过标准输入输出通信适合接命令行工具、本地数据库、文件索引这类东西。远程型走 HTTP适合接团队共享的服务。我的建议是从零个开始需要了再加。原因很现实每挂一个 MCP 服务就多一批工具描述进上下文模型的注意力被摊薄选错工具的概率上升。我一开始图新鲜挂了四个结果它经常在明明该用内置搜索的时候去调外部服务绕一大圈。后来砍到只剩一个——一个我确实每天要用的文档检索服务——行为立刻稳了。提示挂上 MCP 之后记得在AGENTS.md里写一句“什么场景下该用哪个工具”。光靠工具描述模型分不清优先级。6.2 自定义 agent用前置元数据把行为钉住除了默认的两个模式还可以自己定义 agent。定义文件就是一个带前置元数据的 markdown放在项目的.opencode/agent/目录下--- description: 只读代码审查不修改任何文件 mode: subagent tools: write: false edit: false bash: false --- 你是一名严格的代码审查者。只输出问题和建议不输出改好的代码。 审查时按以下顺序 1. 正确性边界条件、空值、异常路径 2. 一致性是否符合项目现有的错误处理约定 3. 可读性命名、函数长度、嵌套深度 每条问题必须给出文件路径和行号无法定位的问题不要提。这里的关键是tools那一段——把工具关掉比在提示词里写“请不要修改文件”可靠得多。提示词是软约束模型有时候会“为了帮你”破例工具开关是硬约束没有工具它就是想改也改不了。我定义了两个一个只读审查一个专门写测试。写测试那个我把编辑权限限定在测试目录防止它顺手改了被测代码——这事真发生过它为了让测试通过把断言改了。6.3 opencode run 和 serve从交互走向流水线交互界面只是入口之一。非交互跑一次很简单opencode run 检查 src 下所有导出是否有对应的类型声明只报告不修改这条命令适合放进定时任务、提交前钩子或者一次性检查。输出是纯文本能直接管道给别的工具。再进一步是起一个本地服务opencode serve --port 4096它会暴露一套 HTTP 接口你可以在自己的脚本或者其他程序里驱动它。我用这个做过一件事把“每次提交前自动生成变更摘要”接进流程提交的时候顺手就把摘要写进变更记录里。这类自动化的价值不在省时间在于它每次都做不会忘。7. 四个真实报错的排查链路7.1 启动后界面花屏、按键没反应这个我遇到过两次两次原因不同。第一次是终端对不上。我 SSH 到一台机器TERM是xterm但实际能力不足制表符全乱。排查顺序是先echo $TERM看值再在一个干净终端不走 tmux里跑一次。如果干净终端正常问题就在复用层去查 tmux 的 terminal 配置如果都不正常就是终端本身的能力问题换个终端或者显式设置TERMxterm-256color试试。第二次更隐蔽按键能显示但命令不响应。原因是我在配置里改键位的时候把两个功能映射到了同一个键上冲突了。这个排查靠的是临时把配置文件挪走跑默认配置——好了就说明是配置的锅。“把配置清空再试”永远是排查 TUI 类问题的第一步能砍掉一半可能性。7.2 一直 401 或 404但密钥明明是对的这个坑我踩过三次每次都浪费不少时间所以整理成了一条固定的排查链现象最可能的原因验证方式401密钥没被正确读取或引用语法错了换成直接写密钥试一次能通就是引用问题404baseURL路径后缀不对或模型 ID 写错先换baseURL再opencode models核对 ID400请求体字段不被支持看错误详情里提到的字段名去掉自定义参数超时网络路径问题或服务端限流用最简请求测一次排除上下文太大关键心法是一次只改一个变量。我见过有人一口气改密钥、改地址、改模型然后通了但根本不知道是哪一步修好的下次还得重新猜。7.3 它说“已修改”git diff 却是空的这个现象第一次遇到时我以为是幻觉。实际原因有几种。最常见的是它改在了别的地方。如果你的会话是在父目录启动的而项目在子目录里它可能把文件写到了另一个同名路径下。排查方法让它把改动文件的绝对路径打出来跟pwd对一下。第二种是改动被它自己回滚了。有些流程里它会先写再验证验证失败就撤回但回复里只说了前半段。这种情况看会话流里有没有后续的失败信息。第三种是写权限被规则拦了但没报错。这个最少见也最坑表现是它认为自己改了实际文件没动。查配置文件里的permission段看edit是不是被设成了ask而你在批量模式下没注意到确认弹窗。7.4 会话跑到后半程开始胡说长会话后期质量下降几乎是必然的。表现是忘记之前定好的约束、重复读同一个文件、给出跟前面矛盾的结论。我的应对是三步。第一步/compact压缩一次看能不能续上。第二步如果压缩后还是不行就开新会话在开头把当前进展、已经确认的结论、还剩什么问题用一小段话交代清楚比让它硬撑强得多。第三步长期来看把稳定的约束从会话里挪到AGENTS.md里——会话会被压缩、会被丢弃项目说明文件不会。注意判断“该开新会话了”的信号我用的是“它开始重复问同一个问题”。这个信号比“回答变慢”更早出现也更准。8. 用了大半年后我改掉的几个习惯8.1 不再让它一次改十个文件一开始我觉得批量改效率高一次说“把这几个模块的日志调用统一一下”它刷地改十几个文件。结果呢验证挂了我得在十几个 diff 里找是哪一处引入的。现在我改成分批一次三到五个文件改完立刻验证通过再下一批。总耗时其实差不多但出问题时定位成本差一个数量级。这个经验和写代码是一个道理——提交粒度小回滚才便宜。8.2 不再截断报错信息我以前贴报错有个坏习惯只贴最后一行。比如“类型不匹配”前面的上下文全砍掉。它拿到这一行只能瞎猜猜错的概率很高。后来我改成贴完整输出包括命令本身、完整的错误栈、涉及的代码位置。上下文长度确实涨了但一次说清楚比来回三轮省得多。这条经验我觉得可以推广到所有跟 agent 的沟通信息给全别让它猜。8.3 不再省略验收标准最开始我提需求是这样的“把这个函数的错误处理改一下”。它改完说完成我一看改是改了但把我原来特意保留的兜底分支删了——因为它觉得那是冗余代码。现在我提需求一定带上验收条件“改完之后npm run typecheck和npm test必须全绿不要动返回值的类型保留现有的兜底分支”。多写这几句来回次数明显下降。模糊的指令换来的不是灵活性是返工。8.4 一个我最近才开始用的小技巧会话跑得久了我会在中间插一句“总结一下当前的进展和还没解决的问题”让它自己复盘一次。这句话有两个作用一是把散落在上下文里的状态收拢方便我检查它理解得对不对二是这个总结本身会成为后续推理的锚点比让它自己在一堆历史里捞要稳。另外还有个小习惯每个任务开始前让它先复述一遍需求。听起来啰嗦但复述错了立刻就能发现比改完一堆文件才发现理解偏了强太多。我印象里有一次它复述的需求里多了个我压根没提的“顺便重构一下”当场就被我拦下来了——如果没有这一步那次重构大概率会顺手把接口签名改了。