OpenCode Agent 核心实现:工具系统与 MCP 协议深度解析 | 深度解析(三)
约 8 分钟2332 字1 次阅读

OpenCode Agent 核心实现:工具系统与 MCP 协议深度解析
引言
在上一篇文章中,我们深入剖析了 OpenCode 的技术架构,理解了其模块化设计和 Provider 模式的多模型支持机制。本文将聚焦于 OpenCode 最重要的核心能力——Agent 工具系统,探讨它如何实现一个真正能够"动手编程"的 AI Agent。
工具系统是 AI 编程助手与传统对话式 AI 的本质区别。一个强大的工具系统让 AI 不仅能够回答问题,还能够执行命令、读写文件、搜索代码、甚至启动子 Agent 来完成复杂任务。
一、工具接口设计
1.1 BaseTool 接口
OpenCode 所有工具都实现了统一的 BaseTool 接口:
// internal/llm/tools/tools.go
type BaseTool interface {
Info() ToolInfo
Run(ctx context.Context, params ToolCall) (ToolResponse, error)
}
这个设计遵循了面向接口编程的原则:
Info()方法返回工具的元信息(名称、描述、参数),用于向 LLM 描述工具能力Run()方法执行具体的工具逻辑
1.2 工具信息结构
type ToolInfo struct {
Name string // 工具名称,LLM 调用时使用
Description string // 工具描述,帮助 LLM 理解何时使用
Parameters map[string]any // JSON Schema 格式的参数定义
Required []string // 必填参数列表
}
type ToolCall struct {
ID string // 工具调用 ID,用于匹配响应
Name string // 工具名称
Input string // JSON 格式的参数
}
type ToolResponse struct {
Type toolResponseType // 响应类型:text 或 image
Content string // 响应内容
Metadata string // 额外的元数据(JSON 格式)
IsError bool // 是否为错误响应
}
1.3 上下文传递机制
工具执行时需要访问当前会话的上下文信息,OpenCode 使用 Go 的 context 机制传递:
// 上下文 key 定义
const (
SessionIDContextKey sessionIDContextKey = "session_id"
MessageIDContextKey messageIDContextKey = "message_id"
)
// 从 context 中提取会话和消息 ID
func GetContextValues(ctx context.Context) (string, string) {
sessionID := ctx.Value(SessionIDContextKey)
messageID := ctx.Value(MessageIDContextKey)
if sessionID == nil {
return "", ""
}
if messageID == nil {
return sessionID.(string), ""
}
return sessionID.(string), messageID.(string)
}
这种设计的巧妙之处在于:
- 线程安全:context 是 Go 语言标准的并发安全机制
- 传递方便:通过函数参数传递,无需修改工具函数签名
- 可测试性:可以在测试中轻松注入 mock context
二、内置工具集详解
OpenCode 的 Coder Agent 配备了 11 个内置工具:
// internal/llm/agent/tools.go
func CoderAgentTools(...) []tools.BaseTool {
return append(
[]tools.BaseTool{
tools.NewBashTool(permissions), // 执行 shell 命令
tools.NewEditTool(lspClients, ...), // 编辑文件
tools.NewFetchTool(permissions), // 获取 URL 内容
tools.NewGlobTool(), // 文件模式匹配
tools.NewGrepTool(), // 文本搜索
tools.NewLsTool(), // 目录列表
tools.NewSourcegraphTool(), // 公共代码搜索
tools.NewViewTool(lspClients), // 查看文件
tools.NewPatchTool(lspClients, ...), // 应用补丁
tools.NewWriteTool(lspClients, ...), // 写文件
NewAgentTool(sessions, messages, lspClients), // 子 Agent
},
GetMcpTools(ctx, permissions)..., // MCP 扩展工具
)
}
2.1 BashTool:安全执行 shell 命令
BashTool 是最强大但也最危险的工具。OpenCode 实现了多层安全机制:
2.1.1 禁止命令列表
// internal/llm/tools/bash.go
var bannedCommands = []string{
"alias", "curl", "curlie", "wget", "axel", "aria2c",
"nc", "telnet", "lynx", "w3m", "links", "httpie", "xh",
"http-prompt", "chrome", "firefox", "safari",
}
禁止的命令主要包括:
- 网络下载工具:防止下载恶意脚本
- 交互式浏览器:防止启动可能不安全的浏览器
- 网络工具:防止网络扫描或攻击
2.1.2 安全只读命令白名单
var safeReadOnlyCommands = []string{
"ls", "echo", "pwd", "date", "cal", "uptime", "whoami", "id", "groups",
"env", "printenv", "set", "unset", "which", "type", "whereis",
"whatis", "uname", "hostname", "df", "du", "free", "top", "ps", "kill",
// Git 命令
"git status", "git log", "git diff", "git show", "git branch",
"git tag", "git remote", "git ls-files", "git ls-remote",
"git rev-parse", "git config --get", "git config --list",
// Go 命令
"go version", "go help", "go list", "go env", "go doc",
"go vet", "go fmt", "go mod", "go test", "go build", "go run",
}
2.1.3 超时控制
const (
DefaultTimeout = 1 * 60 * 1000 // 默认 1 分钟
MaxTimeout = 10 * 60 * 1000 // 最大 10 分钟
)
type BashParams struct {
Command string `json:"command"`
Timeout int `json:"timeout"` // 毫秒
}
2.1.4 输出长度限制
const MaxOutputLength = 30000 // 最大 30000 字符
2.1.5 持久化 shell 会话
BashTool 使用持久化的 shell 会话,这意味着:
- 环境变量在命令间保持
- 当前目录在命令间保持
- 可以建立并切换到某个虚拟环境
// 好的实践
pytest /foo/bar/tests
// 不好的实践
cd /foo/bar && pytest tests // 应该避免 cd 命令
2.2 文件操作工具组
OpenCode 提供了一组互补的文件操作工具:
| 工具 | 用途 | 特点 |
|---|---|---|
glob | 模式匹配文件 | 支持 **/*.go 这样的通配符 |
grep | 文本搜索 | 支持正则表达式和文件过滤 |
ls | 目录列表 | 支持忽略模式 |
view | 查看文件 | 支持分页(offset/limit) |
write | 写入文件 | 完整覆盖 |
edit | 编辑文件 | 替换指定字符串 |
patch | 应用补丁 | 使用 unified diff 格式 |
ViewTool 的 LSP 集成
ViewTool 不仅仅是读取文件,还能通过 LSP 获取额外信息:
// internal/llm/tools/view.go
// 集成 LSP 获取:
// - 语法高亮信息
// - 符号定义位置
// - 悬停文档
2.3 FetchTool:安全的网络访问
// internal/llm/tools/fetch.go
type FetchParams struct {
URL string `json:"url"`
Format string `json:"format"` // "text" 或 "json"
Timeout int `json:"timeout"`
}
特点:
- 需要权限确认
- 支持文本和 JSON 格式
- 可配置超时
2.4 SourcegraphTool:公共代码搜索
// 用于在公共代码仓库中搜索
// 支持搜索代码模式、符号等
2.5 AgentTool:子 Agent 机制
这是最强大的工具,允许 Agent 启动子 Agent 来完成任务:
// internal/llm/agent/agent-tool.go
type agentTool struct {
sessions session.Service
messages message.Service
lspClients map[string]*lsp.Client
}
type AgentParams struct {
Prompt string `json:"prompt"`
}
子 Agent 的特点
- 无状态:每个子 Agent 调用是独立的,不能与主 Agent 通信
- 受限工具集:子 Agent 只能使用只读工具(glob、grep、ls、view)
- 成本累积:子 Agent 的成本会累加到父会话
func (b *agentTool) Run(ctx context.Context, call tools.ToolCall) (tools.ToolResponse, error) {
// 创建新的 Task Agent
agent, err := NewAgent(config.AgentTask, b.sessions, b.messages, TaskAgentTools(b.lspClients))
// 创建任务会话
session, err := b.sessions.CreateTaskSession(ctx, call.ID, sessionID, "New Agent Session")
// 运行 Agent
done, err := agent.Run(ctx, session.ID, params.Prompt)
result := <-done
// 成本累加到父会话
parentSession.Cost += updatedSession.Cost
return tools.NewTextResponse(response.Content().String()), nil
}
使用场景
文档建议在以下场景使用 AgentTool:
- 搜索关键词如 "config" 或 "logger"
- 询问"哪个文件做了 X"
- 不确定第一次就能找到匹配项的情况
但要注意:
子 Agent 不能使用 Bash、Replace、Edit,所以不能修改文件。如果需要修改文件,应该直接使用相应工具。
三、工具执行流程
3.1 Agent 处理循环
回顾 Agent 的核心处理循环:
// internal/llm/agent/agent.go
func (a *agent) processGeneration(ctx context.Context, sessionID, content string, ...) AgentEvent {
for {
// 1. 流式处理 LLM 事件
agentMessage, toolResults, err := a.streamAndHandleEvents(ctx, sessionID, msgHistory)
// 2. 如果需要工具调用
if (agentMessage.FinishReason() == message.FinishReasonToolUse) && toolResults != nil {
// 3. 将工具结果添加到历史
msgHistory = append(msgHistory, agentMessage, *toolResults)
// 4. 继续循环
continue
}
// 5. 否则返回最终响应
return AgentEvent{
Type: AgentEventTypeResponse,
Message: agentMessage,
Done: true,
}
}
}
3.2 事件处理
// internal/llm/agent/agent.go - processEvent
func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg *message.Message, event provider.ProviderEvent) error {
switch event.Type {
case provider.EventThinkingDelta:
// Claude 的扩展思考内容
assistantMsg.AppendReasoningContent(event.Content)
case provider.EventContentDelta:
// 普通文本内容
assistantMsg.AppendContent(event.Content)
case provider.EventToolUseStart:
// 开始工具调用
assistantMsg.AddToolCall(*event.ToolCall)
case provider.EventToolUseStop:
// 工具调用完成
assistantMsg.FinishToolCall(event.ToolCall.ID)
case provider.EventComplete:
// 生成完成
assistantMsg.SetToolCalls(event.Response.ToolCalls)
assistantMsg.AddFinish(event.Response.FinishReason)
}
return nil
}
3.3 工具匹配与执行
// internal/llm/agent/agent.go - streamAndHandleEvents
toolCalls := assistantMsg.ToolCalls()
for i, toolCall := range toolCalls {
// 1. 查找工具
var tool tools.BaseTool
for _, availableTool := range a.tools {
if availableTool.Info().Name == toolCall.Name {
tool = availableTool
break
}
}
// 2. 工具未找到
if tool == nil {
toolResults[i] = message.ToolResult{
ToolCallID: toolCall.ID,
Content: fmt.Sprintf("Tool not found: %s", toolCall.Name),
IsError: true,
}
continue
}
// 3. 执行工具
toolResult, toolErr := tool.Run(ctx, tools.ToolCall{
ID: toolCall.ID,
Name: toolCall.Name,
Input: toolCall.Input,
})
// 4. 处理结果
toolResults[i] = message.ToolResult{
ToolCallID: toolCall.ID,
Content: toolResult.Content,
IsError: toolErr != nil || toolResult.IsError,
}
}
3.4 权限检查
敏感操作需要权限确认:
// internal/permission/permission.go
type Permission string
const (
PermissionBash Permission = "bash"
PermissionEdit Permission = "edit"
PermissionWrite Permission = "write"
PermissionRead Permission = "read"
PermissionFetch Permission = "fetch"
)
// 权限服务接口
type Service interface {
Check(p Permission) error // 检查是否有权限
Grant(p Permission) // 单次授予
GrantPersistant(p Permission) // 永久授予
Deny(p Permission) // 拒绝
IsGranted(p Permission) bool // 是否已授权
IsPersistant(p Permission) bool // 是否永久授权
}
当权限被拒绝时:
if errors.Is(toolErr, permission.ErrorPermissionDenied) {
toolResults[i] = message.ToolResult{
ToolCallID: toolCall.ID,
Content: "Permission denied",
IsError: true,
}
// 取消后续所有工具调用
for j := i + 1; j < len(toolCalls); j++ {
toolResults[j] = message.ToolResult{
ToolCallID: toolCalls[j].ID,
Content: "Tool execution canceled by user",
IsError: true,
}
}
a.finishMessage(ctx, &assistantMsg, message.FinishReasonPermissionDenied)
break
}
四、MCP 协议集成
4.1 MCP 简介
Model Context Protocol (MCP) 是一种开放协议,允许 AI 模型与外部工具服务器通信。OpenCode 通过 mcp-go 库实现了 MCP 客户端。
4.2 MCP 工具封装
// internal/llm/agent/mcp-tools.go
type mcpTool struct {
mcpName string // MCP 服务器名称
tool mcp.Tool // MCP 工具定义
mcpConfig config.MCPServer // MCP 服务器配置
permissions permission.Service
}
func (b *mcpTool) Info() tools.ToolInfo {
required := b.tool.InputSchema.Required
if required == nil {
required = make([]string, 0)
}
return tools.ToolInfo{
Name: fmt.Sprintf("%s_%s", b.mcpName, b.tool.Name),
Description: b.tool.Description,
Parameters: b.tool.InputSchema.Properties,
Required: required,
}
}
命名规范:MCP 工具的名称格式为 {mcpName}_{toolName},例如 filesystem_read 或 github_get_issue。
4.3 MCP 工具执行
func runTool(ctx context.Context, c MCPClient, toolName string, input string) (tools.ToolResponse, error) {
defer c.Close()
// 1. 初始化 MCP 会话
initRequest := mcp.InitializeRequest{}
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
initRequest.Params.ClientInfo = mcp.Implementation{
Name: "OpenCode",
Version: version.Version,
}
_, err := c.Initialize(ctx, initRequest)
if err != nil {
return tools.NewTextErrorResponse(err.Error()), nil
}
// 2. 调用工具
toolRequest := mcp.CallToolRequest{}
toolRequest.Params.Name = toolName
var args map[string]any
if err = json.Unmarshal([]byte(input), &args); err != nil {
return tools.NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
}
toolRequest.Params.Arguments = args
result, err := c.CallTool(ctx, toolRequest)
if err != nil {
return tools.NewTextErrorResponse(err.Error()), nil
}
// 3. 处理结果
output := ""
for _, v := range result.Content {
if v, ok := v.(mcp.TextContent); ok {
output = v.Text
} else {
output = fmt.Sprintf("%v", v)
}
}
return tools.NewTextResponse(output), nil
}
4.4 MCP 服务器配置
// internal/config/config.go
type MCPServer struct {
Command string `json:"command"` // stdio 模式命令
Env []string `json:"env"` // 环境变量
Args []string `json:"args"` // 命令参数
Type MCPType `json:"type"` // stdio 或 sse
URL string `json:"url"` // SSE 模式 URL
Headers map[string]string `json:"headers"` // HTTP 头
}
配置示例
{
"mcpServers": {
"filesystem": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed"]
},
"github": {
"type": "sse",
"url": "https://api.example.com/mcp",
"headers": {
"Authorization": "Bearer token"
}
}
}
}
4.5 MCP 连接类型
STDIO 模式
适用于本地命令启动的 MCP 服务器:
// 通过 stdio 通信
type stdioClient struct {
cmd *exec.Cmd
stdin io.WriteCloser
stdout io.ReadCloser
}
SSE 模式
适用于远程 MCP 服务器:
// 通过 Server-Sent Events 通信
type sseClient struct {
url string
headers map[string]string
}
五、工具描述的优化
5.1 BashTool 的详细描述
BashTool 的描述非常详细,包含使用指南和最佳实践:
func bashDescription() string {
return fmt.Sprintf(`Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
Before executing the command, please follow these steps:
1. Directory Verification:
- If the command will create new directories or files, first use the LS tool to verify the parent directory exists and is the correct location
- For example, before running "mkdir foo/bar", first use LS to check that "foo" exists and is the intended parent directory
2. Security Check:
- For security and to limit the threat of a prompt injection attack, some commands are limited or banned. If you use a disallowed command, you will receive an error message explaining the restriction. Explain the error to the User.
- Verify that the command is not one of the banned commands: %s.
3. Command Execution:
- After ensuring proper quoting, execute the command.
- Capture the output of the command.
4. Output Processing:
- If the output exceeds %d characters, output will be truncated before being returned to you.
- Prepare the output for display to the user.
5. Return Result:
- Provide the processed output of the command.
- If any errors occurred during execution, include those in the output.
Usage notes:
- The command argument is required.
- You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 30 minutes.
- VERY IMPORTANT: You MUST avoid using search commands like 'find' and 'grep'. Instead use Grep, Glob, or Agent tools to search.
- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines.
- IMPORTANT: All commands share the same shell session. Shell state persists between commands.`, bannedCommandsStr, MaxOutputLength)
}
5.2 参数定义
工具的参数使用 JSON Schema 定义:
type BashParams struct {
Command string `json:"command"`
Timeout int `json:"timeout"`
}
// 对应的 ToolInfo.Parameters
Parameters: map[string]any{
"command": map[string]any{
"type": "string",
"description": "The bash command to execute",
},
"timeout": map[string]any{
"type": "integer",
"description": "Timeout in milliseconds",
},
}
Required: []string{"command"}
六、工具系统的设计哲学
6.1 单一职责
每个工具只做一件事,且做好:
BashTool只执行命令GlobTool只做文件模式匹配GrepTool只做文本搜索
这种设计让工具更容易理解和维护。
6.2 工具组合
复杂的操作通过组合工具实现:
# 创建目录并进入
mkdir new_project && cd new_project
# 编译并运行
go build ./... && go test ./...
6.3 错误处理
工具执行可能失败,OpenCode 统一处理:
type ToolResponse struct {
Content string `json:"content"`
IsError bool `json:"is_error"`
}
// 使用 NewTextErrorResponse 创建错误响应
func NewTextErrorResponse(content string) ToolResponse {
return ToolResponse{
Content: content,
IsError: true,
}
}
6.4 权限最小化
默认情况下,敏感操作需要用户确认:
// TUI 层处理权限对话框
case pubsub.Event[permission.PermissionRequest]:
a.showPermissions = true
return a, a.permissions.SetPermissions(msg.Payload)
用户可以选择:
- Allow:单次允许
- Allow for session:本次会话允许
- Deny:拒绝
七、与 LLM 的协作
7.1 工具选择逻辑
LLM 根据工具描述决定使用哪个工具:
If you are searching for a keyword like "config" or "logger", or for questions like "which file does X?", the Agent tool is strongly recommended.
If you want to read a specific file path, use the View or GlobTool tool instead of the Agent tool.
7.2 工具调用循环
for {
// 1. LLM 决定调用工具
agentMessage, toolResults, err := a.streamAndHandleEvents(ctx, sessionID, msgHistory)
// 2. 如果 LLM 请求工具调用
if (agentMessage.FinishReason() == message.FinishReasonToolUse) && toolResults != nil {
// 3. 执行工具并获取结果
msgHistory = append(msgHistory, agentMessage, *toolResults)
// 4. 继续,让 LLM 处理结果
continue
}
// 5. 否则返回最终响应
return AgentEvent{...}
}
7.3 思考过程(Claude Extended Thinking)
对于 Claude 模型,OpenCode 支持扩展思考模式:
// internal/llm/provider/anthropic.go
if messageContent != "" && a.options.shouldThink != nil && a.options.shouldThink(messageContent) {
thinkingParam = anthropic.ThinkingConfigParamOfEnabled(int64(float64(a.providerOptions.maxTokens) * 0.8))
temperature = anthropic.Float(1)
}
思考过程通过 EventThinkingDelta 事件传递:
case provider.EventThinkingDelta:
assistantMsg.AppendReasoningContent(event.Content)
八、实际使用示例
8.1 简单文件操作
User: Create a new file called hello.go with a main function that prints "Hello, World!"
LLM calls: write(file_path="hello.go", content="package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}")
8.2 搜索和编辑
User: Find all TODO comments in the project and add a timestamp
LLM calls:
1. grep(pattern="TODO", include="*.go")
2. edit(file_path="file1.go", old_string="TODO", new_string="TODO - 2024-01-15")
3. edit(file_path="file2.go", old_string="TODO", new_string="TODO - 2024-01-15")
8.3 使用子 Agent
User: Find all files that import the "database/sql" package
LLM calls:
agent(prompt="Search all Go files in the project that import "database/sql". Return a list of file paths.")
8.4 复杂任务
User: Run the test suite and fix any failing tests
LLM calls:
1. bash(command="go test ./... 2>&1")
2. view(file_path="package_test.go", offset=10, limit=50)
3. edit(file_path="package_test.go", ...)
4. bash(command="go test ./... -run TestName")
结语
OpenCode 的工具系统是其作为 AI 编程助手的核心能力。通过:
- 统一的工具接口:任何工具都实现
BaseTool接口 - 丰富的内置工具:覆盖文件操作、代码搜索、命令执行、子 Agent
- MCP 扩展协议:支持接入任何 MCP 兼容的外部工具
- 细粒度的权限控制:用户始终掌控 AI 能做什么
- 详细工具描述:帮助 LLM 做出正确的工具选择
这套工具系统让 OpenCode 不仅仅是一个问答系统,而是一个真正能够动手编程的 AI Agent。
本系列下一篇文章将深入探讨 OpenCode 的数据库与会话管理,剖析 SQLite 存储设计、自动摘要压缩机制,以及如何实现长期记忆能力。