第四次作业
| 这个作业属于哪个课程 | https://home.cnblogs.com/u/lokijinjin/relation/schoolclasses |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS/homework/16760 |
| 这个作业的目标 | 理解智能体的交互与执行机制,构建能通过自然语言调用工具完成任务的 AI 助手 |
| 学号 | 王嵘烨 3124004179,李建鑫3124004170 ,周微 3124004189 |
TopViewbot:一个“能说会做”的 AI 智能体
一、项目简介与选题背景
学习和开发中,我们经常需要整理资料、修改文件、执行命令服务器。TopViewbot 面向这些场景,让用户通过自然语言描述目标,由智能体选择工具完成操作,并在网页中反馈结果。
系统基于 OpenCode 智能体核心,结合 Web 界面、模型服务、MCP 接入与浏览器控制。自然语言对话体现“能说”,文件读写、命令执行等工具体现“会做”。项目复用了 OpenCode 等开源组件。

二、项目目录结构
以下列出主要模块:
TopViewbot/
├── web/ # Vue 网页界面
├── opencode/packages/opencode/src/
│ ├── session/ # 对话与执行循环
│ ├── tool/ # 内置工具
│ └── mcp/ # MCP 客户端
├── packages/
│ ├── topviewbot/ # 启动、配置与技能
│ ├── browser-mcp-server/ # 浏览器桥接
│ └── browser-extension/ # 浏览器扩展
└── docs/ # 项目文档
三、技术选型与总体架构
项目主要使用 TypeScript,前端采用 Vue 3 和 Vite,后端使用 Bun、Hono,并通过 AI SDK 对接模型服务。MCP 模块使用官方 SDK,支持本地进程与远程服务接入。会话、消息等数据通过本地文件持久化保存。
前端负责输入和展示,核心负责组织上下文、处理模型输出、调度工具。模型选择工具,具体操作由程序执行。内置工具与外部 MCP 工具共同构成可用能力。
网页还提供模型切换、文件上传与预览、MCP 管理等入口,将对话和操作结果放在同一界面中。用户可以围绕当前会话的工作目录继续处理文件,减少在聊天窗口与本地工具之间反复切换。

四、业务流程与 Agent 执行机制
用户发送指令后,系统装载会话历史、用户偏好和工具描述。模型结合这些信息决定直接回答,或生成工具调用。执行结果会加入上下文,供模型判断是否继续操作,最后形成回复。
这一循环让智能体能够连续完成多个步骤。前端通过 SSE 更新消息与工具状态,用户可以看到执行进展,也可以中止任务。工具错误会被记录,后续处理仍需结合模型判断和权限规则。
五、核心功能实现与关键代码
1. 对话与 MCP 工具调用
session/llm.ts 使用 streamText,将历史消息、系统提示词和可用工具交给模型。MCP 模块通过 listTools() 获取工具,再将描述、输入结构和执行函数适配为模型可调用的工具。
内置工具统一注册,外部工具动态发现;只有已连接 MCP 服务的工具会加入可用集合。
mcp/index.ts 中的工具适配与调用逻辑如下:
async function convertMcpTool(
serverName: string,
mcpTool: MCPToolDef,
client: MCPClient,
timeout?: number,
): Promise<Tool> {
const inputSchema = mcpTool.inputSchema
// Spread first, then override type to ensure it's always "object"
const schema: JSONSchema7 = {
...(inputSchema as JSONSchema7),
type: "object",
properties: (inputSchema.properties ?? {}) as JSONSchema7["properties"],
additionalProperties: false,
}
return dynamicTool({
description: mcpTool.description ?? "",
inputSchema: jsonSchema(schema),
execute: async (args: unknown) => {
try {
return await client.callTool(
{
name: mcpTool.name,
arguments: args as Record<string, unknown>,
},
CallToolResultSchema,
{
resetTimeoutOnProgress: true,
timeout,
},
)
} catch (error) {
if (!isAuthenticationError(error)) {
throw error
}
const message = error instanceof Error ? error.message : String(error)
await markFailed(serverName, message)
const cfg = await Config.get()
const entry = cfg.mcp?.[serverName]
if (!entry || !isMcpConfigured(entry)) {
throw error
}
const recovered = await ensureServerConnected(serverName, entry, { interactiveAuth: true })
if (!recovered) {
throw error
}
const refreshedClient = (await clients())[serverName]
if (!refreshedClient) {
throw error
}
return refreshedClient.callTool(
{
name: mcpTool.name,
arguments: args as Record<string, unknown>,
},
CallToolResultSchema,
{
resetTimeoutOnProgress: true,
timeout,
},
)
}
},
})
}
函数将工具描述和参数结构交给模型,在执行时发送工具名与参数;认证错误会触发连接恢复与重试,其他错误继续上抛。调用结果返回执行循环。
2. 文件操作与权限控制
tool/write.ts 接收文件路径与内容,在写入前检查目录、已有文件状态和操作权限。关键代码为:
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(ctx.cwd, params.filePath)
await assertExternalDirectory(ctx, filepath)
const file = Bun.file(filepath)
const exists = await file.exists()
const contentOld = exists ? await file.text() : ""
if (exists) await FileTime.assert(ctx.sessionID, filepath)
const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, params.content))
await ctx.ask({
permission: "edit",
patterns: [path.relative(Instance.worktree, filepath)],
always: ["*"],
metadata: {
filepath,
diff,
},
})
await Bun.write(filepath, params.content)
await Bus.publish(File.Event.Edited, {
file: filepath,
})
await Bus.publish(FileWatcher.Event.Updated, {
file: filepath,
event: exists ? "change" : "add",
})
FileTime.read(ctx.sessionID, filepath)
权限模块根据规则决定放行、询问或拒绝,并非每次都弹出确认。通过检查后,程序才将内容写入文件,实现从语言指令到实际产物的转换。
3. 多轮对话与用户偏好
系统按会话保存并装载历史,实现连续交流。session/instruction.ts 从本地文件读取偏好,加入缓存,并处理文件缺失和解析失败:
async function loadTopViewbotPreferences(): Promise<string> {
const now = Date.now()
// 检查缓存是否有效
if (cachedPreferencesPrompt !== '' && now - preferencesLastLoadTime < PREFERENCES_CACHE_TTL) {
return cachedPreferencesPrompt
}
const preferencesPath = process.env.TOPVIEWBOT_PREFERENCES_PATH
if (!preferencesPath) {
// 回退到旧的环境变量方式
return process.env.TOPVIEWBOT_PREFERENCES_PROMPT || ''
}
try {
const file = Bun.file(preferencesPath)
if (!await file.exists()) {
cachedPreferencesPrompt = ''
preferencesLastLoadTime = now
return ''
}
const content = await file.text()
const data = JSON.parse(content)
if (!data.preferences?.length) {
cachedPreferencesPrompt = ''
preferencesLastLoadTime = now
return ''
}
const items = data.preferences.map((p: any, i: number) => `${i + 1}. ${p.content}`).join('\n')
cachedPreferencesPrompt = `<user-preferences>
以下是用户设置的偏好,请在回复中遵循这些偏好:
${items}
这些偏好由用户明确设置,优先级高于默认行为。
</user-preferences>`
preferencesLastLoadTime = now
return cachedPreferencesPrompt
} catch (e) {
log.warn("Failed to load TopViewbot preferences", { error: e })
// 返回旧缓存或空字符串
return cachedPreferencesPrompt || process.env.TOPVIEWBOT_PREFERENCES_PROMPT || ''
}
}
读取后,偏好被加入系统提示词:
const preferencesPrompt = await loadTopViewbotPreferences()
if (preferencesPrompt) {
result.push(preferencesPrompt)
}
会话历史帮助理解“修改刚才的文件”,偏好用于传递“使用中文回复”等习惯。长对话还需考虑上下文长度与压缩造成的信息损失。
4. 浏览器自动化控制
tool/browser.ts 提供导航、页面快照、点击、表单填写等工具。桥接层将调用转为浏览器操作,页面导航的关键代码为:
export const BrowserNavigateTool = Tool.define("browser_navigate", {
description: `Navigate a browser tab: go to URL, back, forward, reload, or manage tabs.
Parameters:
- tabId: Tab ID (from browser_status)
- action: "goto" (default), "back", "forward", "reload", "new_tab", "close_tab"
- url: Required for "goto" and optional for "new_tab"`,
parameters: z.object({
tabId: z.string().describe("Tab ID"),
browser: browserParam,
url: z.string().optional().describe("URL to navigate to (required for goto)"),
action: z
.enum(["goto", "back", "forward", "reload", "new_tab", "close_tab"])
.optional()
.describe('Navigation action (default: "goto")'),
}),
async execute(params, ctx) {
await ctx.ask({
permission: "browser_navigate",
patterns: [params.url ?? "*"],
always: ["*"],
metadata: { tabId: params.tabId, url: params.url },
})
const bridge = requireBridge()
const result = await bridge.navigate(
params.tabId,
{ url: params.url, action: params.action },
params.browser as BrowserTarget | undefined,
)
const action = params.action ?? "goto"
const msg =
action === "goto"
? `Navigated to ${params.url}`
: action === "new_tab"
? `New tab created${result.tabId ? ` (ID: ${result.tabId})` : ""}`
: `Action: ${action}`
return {
title: msg,
output: msg,
metadata: { newTabId: result.tabId },
}
},
})
智能体可以先读取页面快照,再定位元素、执行操作,完成资料检索和网页交互任务。
5. Skills 技能扩展
项目内置 PDF、Word、PPT 等技能。tool/skill.ts 按名称查找技能,检查权限,解析说明文件,并把内容和所在目录返回模型:
async execute(params: z.infer<typeof parameters>, ctx) {
const skill = await Skill.get(params.name)
if (!skill) {
const available = await Skill.all().then((x) => Object.keys(x).join(", "))
throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`)
}
await ctx.ask({
permission: "skill",
patterns: [params.name],
always: [params.name],
metadata: {},
})
// Load and parse skill content
const parsed = await ConfigMarkdown.parse(skill.location)
const dir = path.dirname(skill.location)
// Format output similar to plugin pattern
const output = [`## Skill: ${skill.name}`, "", `**Base directory**: ${dir}`, "", parsed.content.trim()].join("\n")
return {
title: `Loaded skill: ${skill.name}`,
output,
metadata: {
name: skill.name,
dir,
},
}
},
解析后的内容会返回模型,指导后续工具调用。新增技能可以复用专门任务的处理步骤,减少重复描述。
6. 并行会话与状态管理
useParallelSessions.ts 按会话维护运行状态,下面展示 SSE 事件对运行、空闲和错误状态的处理:
function handleGlobalSSEEvent(event: SSEEvent) {
const sessionID = extractSessionID(event)
if (!sessionID) return
switch (event.type) {
case 'session.status':
// status event: { sessionID, status: { type: 'busy' | 'idle' | 'retry' } }
const status = event.properties?.status
if (status) {
runningStates[sessionID] = status.type === 'busy' || status.type === 'retry'
}
break
case 'session.idle':
// Session finished
runningStates[sessionID] = false
break
case 'session.error':
// Session error - also stopped
runningStates[sessionID] = false
break
}
}
前端设置了 10 个并行会话的上限,达到上限时提示等待。SSE 事件同步运行、空闲和错误状态,用户切换会话后仍能跟踪其他任务的进展。
六、使用演示
下图展示实际对话与执行结果

七、当前局限与改进方向
工具选择和参数生成仍依赖模型,复杂任务可能出现理解偏差;外部服务也会受到网络、认证和调用成本影响。后续可完善目录权限、失败恢复和操作回滚,并增加常用任务模板,提升执行的稳定性。
八、GitHub 地址与小组分工
代码仓库:TopViewbot。
| 成员 | 学号 | 分工内容 |
|---|---|---|
| 王嵘烨 | 3124004179 | 负责会话与 Memory 管理部分的梳理,整理聊天历史保存、用户偏好注入、缓存和上下文压缩流程,编写对应实现说明。 |
| 周微 | 3124004189 | 负责 Skills 部分的梳理,整理技能查找、权限检查和说明加载流程,规划后续技能消融实验及提示词 A/B 对比方案。 |
| 李建鑫 | 3124004170 | 负责 MCP 与工具调用部分的梳理,整理工具发现、参数约束、执行回传及认证恢复机制,编写对应实现说明。 |
九、成员心得
| 成员 | 心得 |
|---|---|
| 王嵘烨 | 梳理了会话和 Memory 管理。聊天历史通过 sessionID 区分,用户偏好则从文件读取后加入提示词。看完这两条流程,我理解了多轮追问和跨会话偏好的区别。十秒缓存和历史压缩也让我注意到,保存了信息,还要考虑模型何时能够读到 |
| 周微 | 我主要关注 Skills 的加载。Skill.get 找到技能后,还要检查权限,再读取说明文件交给模型。这让我理解了技能与工具如何配合。后续想固定模型和任务集,做技能开关的消融实验,再做提示词 A/B 对比,记录成功率、Token 和耗时,具体看看效果差在哪里。 |
| 李建鑫 | 这次作业让我系统了解了 MCP。梳理 listTools 和 callTool 后,我理解了工具发现、参数传递和结果回传的过程。印象较深的是工具描述要写清楚,参数也要用 Schema 约束。接口调通后,认证过期、超时和调用失败这些情况也需要处理 |
十、学习资源与参考资料
- MCP 官方客户端教程:学习工具发现与调用流程。
- Hello-Agents:学习智能体原理及实践。
- AI SDK 工具调用文档:理解工具描述、参数结构与执行函数。

浙公网安备 33010602011771号