☰
本地画布+MCP协议:开源实现AI Agent全程操控视频剪辑
2026/9/26 14:48:56 网站建设 项目流程

前阵子有个朋友问我:“你说现在 AI Agent 能写代码、能订机票,能不能让它自己剪一条视频出来?”我说可以,前提是你得给它一个能操控画布和剪辑的服务。于是我把这个本地画布 + 剪辑的工具做成开源项目,并给它接上了 MCP(Model Context Protocol),让任何支持 MCP 的 Agent 都能全程操控它,当成 libTV 的开源平替方案。项目跑通之后,Claude Desktop、Cursor、Trae 这些主流 Agent 客户端,都能对着本地画布指哪打哪,从导入素材、左右移动画布、裁片段、加转场到导出成片,全部由 Agent 端到端完成。

这篇文章我会把整个项目的设计逻辑、架构拆解、部署接入方式、真实调用过程,以及我踩过的坑都写出来。如果你正在研究 Agent 与视频创作工具的联动,或者想找一个能跑在本地的、可被 AI 操控的剪辑工具,这篇应该能帮你省下不少时间。

1. 为什么放着现成的 libTV 不用,我偏要自己搓一个

1.1 libTV 很好,但我需要的是“能被任意 Agent 调用”的能力

libTV 本身的思路很吸引人:通过提示词驱动画布上的视频创作,让 AI 理解时间轴和片段。问题在于,这类工具的提示词体系和交互方式往往和自家产品深度绑定,你用它的提示词格式,就得在它的环境里玩。一旦想把同样的操作交给不同的 Agent 客户端,或者想在自己本地的素材库、自有渲染管线上跑自动化,就发现根本接不进去。

我实际遇到的情况更直接。团队里有人用 libTV 做过几条测试视频,效果确实可以,但当我们想把它并进现有的内容生产流程时,卡住了:第一,素材都在内网服务器上,我不想为了一次剪辑把几十 GB 的原始视频传到某个云端画布;第二,我需要让 Agent 根据我们自己的排期表自动决定剪哪段、用哪个模版,这个逻辑在 libTV 的封闭环境里写不进去。

MCP 协议出现后,这个问题的解法突然清晰了。MCP 本质上是一个标准化的工具调用协议:Agent 可以通过它发现工具、读取工具定义、调用工具并拿到结构化结果。只要我把“画布操作”和“剪辑操作”封装成一组 MCP 工具,那么任何支持 MCP 的 Agent 都能直接操控,不用关心我的底层是 Electron 还是 FFmpeg,也不用为不同 Agent 适配接口。

1.2 这个项目到底是什么:本地画布 + 剪辑 + MCP Server 三件套

我开源的这个项目,核心是三个部分组合在一起。

第一部分是本地画布。它是一个运行在你电脑上的可视化工区,能够展示视频片段在时间轴上的排列方式,支持素材的拖入、片段的左右移动、预览帧的刷新。这里的“画布”不是简单的一块绘图板,而是一个能看到当前剪辑进度的界面。

第二部分是剪辑内核。它负责真正的视频处理:读取媒体文件信息、按时间轴顺序切分片段、合并输出、生成字幕轨道、做转场。底层可以走 FFmpeg 也可以走 WebCodecs,我在项目里做了一层渲染器抽象,方便替换。

第三部分是 MCP Server。它把画布和剪辑能力包装成一组标准工具,供 Agent 调用。画布上发生的每一次变化,都会被同步成一个可查询的项目状态;Agent 通过工具接口读写这个状态,而不是直接操作界面。

这三件套的价值在于:Agent 可以全流程操控,但每一步动作都有明确的语义和返回值。它不是简单地把鼠标坐标传给 Agent 去点,而是让 Agent 理解“这段 clip 是哪条素材的哪一段”“当前画布光标在第几秒”这种结构化信息。

1.3 和 libTV 的核心差异,我用一张表说清楚

对比项libTV 类工具我这个开源方案
交互入口自家产品的提示词MCP 标准工具,多 Agent 通用
素材存储通常在云端或产品私有空间本地文件引用,不上传
扩展性受限于产品功能边界可自己加工具、换渲染器
部署形态在线服务为主本地进程,离线可用
自动化能力受 API 开放程度限制Agent 可编程编排完整剪辑流程
成本模型按服务订阅或点数计费只花本地算力
提示词格式厂商自定义MCP 内置 JSON Schema 描述,Agent 自动理解

这张表可能有些主观,但核心点很明确:如果你需要的只是顺手剪一条 AI 视频,libTV 这类工具完全够用;如果你想做一个让 Agent 自主完成“从素材到成片”的本地自动化管道,MCP 方案的自由度和可控性明显更高。

2. 核心架构拆解:画布状态机、剪辑内核与 MCP Server 如何分工

2.1 画布不是“画板”,是一套可被外部读取的项目状态机

项目最开始我也踩过一个典型的坑:想当然地把画布做成了一块实时渲染区域,然后让 Agent 通过移动鼠标去操作。结果发现这条路走不通,因为 Agent 对像素级的坐标操作根本不可靠,而且非常慢。后来我把画布重新定义为“项目状态的可视化呈现”,核心不再是渲染,而是状态。

项目状态(ProjectState)是一个完整的 JSON 结构,包含素材列表、时间轴片段、画布光标位置、当前选中的片段、字幕、转场配置等。画布界面只是把这个状态渲染出来。Agent 每次调用工具,本质上是在读写这个状态机。

画布左右移动的方法也从这个设计里自然浮现出来了:画布光标指向时间轴上的某个时间点,向左移动 5 秒、向右移动 10 秒,就是一次状态更新。这个操作被封装成 move_cursor 工具,而不是真的去点界面上的箭头按钮。这带来的好处是:Agent 可以精确地知道移动前后光标落在哪里,再配合 get_project_state 工具确认效果,形成完整的操作闭环。libTV 用户常问的“画布左右移动方法”,在我这里其实是一个游标移动语义,而不是某个快捷键。

2.2 剪辑内核:时间轴数据模型要先于渲染确定

剪辑内核我采用了“素材引用 + 时间轴片段 + 渲染队列”三层结构。

素材引用保存的是本地文件的路径、时长、分辨率、编码格式等元信息。时间轴片段则是在素材基础上追加了入点、出点、轨道编号、特效标记。渲染队列负责把当前时间轴状态翻译成具体的合成指令。

之所以强调“数据模型先于渲染”,是因为 Agent 操控剪辑时,永远是在和数据模型打交道。比如说 Agent 想删掉第 10 到第 15 秒的片段,它调用 remove_clip 工具,传入 clip_id,内核负责更新时间轴数据结构。这时候画布界面会收到一个状态变更事件,自动重新渲染。如果先把界面渲染做完了再去想数据,就会遇到界面状态和时间轴数据不同步的难题。

还有一个设计点是素材文件为什么用路径引用而不是直接拷贝到项目目录。原因是视频文件体积太大了,本地做自动化处理时,拷贝文件的时间和磁盘开销都不可接受。直接用路径引用,配合权限校验控制访问范围,能保证效率,又能防止 Agent 乱读系统中的其他文件。

2.3 MCP Server 的桥接层:一切工具都走同一套语义

MCP Server 在这里扮演的角色非常明确:把剪辑内核的功能翻译成 Agent 能理解的语言。MCP 的 tools 定义基于 JSON Schema,包含工具名称、描述、参数结构、返回值结构。Agent 拿到这些定义后,会自动根据用户指令选择合适的工具并填入参数。

这里我选了 TypeScript 来实现 MCP Server,主要是生态比较省心,官方 SDK 用起来也顺手。下面是一个简单的 create_project 工具注册示例:

import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/server/mcp.js"; server.registerTool( "create_project", { title: "创建剪辑项目", description: "在本地画布中创建一个新的视频剪辑项目。创建成功后返回 project_id。后续所有剪辑工具都需要基于该 project_id 操作。", inputSchema: { type: "object", properties: { project_name: { type: "string", description: "项目名称,建议使用语义化名称,例如 '产品介绍_0426'", }, width: { type: "number", description: "画布宽度,默认 1920" }, height: { type: "number", description: "画布高度,默认 1080" }, fps: { type: "number", description: "帧率,默认 30" }, }, required: ["project_name"], }, }, async (args) => { const project = createProject(args); return { content: [ { type: "text", text: JSON.stringify({ project_id: project.id, project_name: project.name, status: "created", }), }, ], }; } );

这里有一个很多人容易忽略的细节:工具描述必须写清楚前置条件和副作用。我见过有人把 create_project 的描述写成“创建项目”,结果 Agent 在已有项目的情况下也频繁调用,反而把状态搞乱了。我在描述里加了“创建后返回 project_id。后续所有剪辑工具都需要基于该 project_id 操作”,就是要让 Agent 知道这个工具应该在最开始调用,并且结果会被后续步骤引用。

用 MCP 而不是自研 REST API 的决定,也是经过实际对比的。自研 API 的问题是:每个 Agent 客户端都要单独接一遍,每家的调用习惯还不一样。而 MCP 是现在各大 AI 工具的标准协议,Claude Desktop、Cursor、Trae、Cline 都原生支持,我只需要写一个 server,所有客户端都能复用,这个性价比要划算得多。

3. 让 Agent 插上手:MCP Server 部署与工具注册实操

3.1 服务端启动:本地进程优先用 stdio 模式

MCP Server 有两种常见运行模式:stdio 模式和 HTTP/SSE 模式。对于本地剪辑工具来说,我强烈建议用 stdio 模式。原因是本地进程通过标准输入输出通信,不需要监听端口,不会有防火墙弹窗或端口冲突问题,而且启动速度快。

启动命令很简单,如果你使用 npm 全局安装,可以这样写:

npm install -g local-cut-studio local-cut-studio --mcp

项目开发阶段,也可以直接用仓库里的启动脚本:

git clone https://github.com/yourname/local-cut-studio.git cd local-cut-studio npm install npm run build node dist/cli.js --mcp

启动后终端会进入 MCP 通信状态,正常情况下不会有太多输出,只有调用工具时才会打印日志。如果看不到任何提示,不要慌,这是正常的,说明它在等你上游 Agent 客户端来连接。

3.2 注册到各类 Agent 客户端:配置文件写法

不同的 Agent 客户端配置 MCP Server 的方式大同小异,核心都是指向同一条命令。最常用的是 Claude Desktop 和 Cursor,它们的配置文件我分别写一下。

Claude Desktop 的配置文件在claude_desktop_config.json里,需要加入 mcpServers 节点:

{ "mcpServers": { "local-cut-studio": { "command": "node", "args": ["/absolute/path/to/local-cut-studio/dist/cli.js", "--mcp"] } } }

Cursor 和 Trae 这类编辑器,通常在设置里的 MCP 面板中添加,配置格式类似:

{ "mcpServers": { "local-cut-studio": { "command": "node", "args": ["/absolute/path/to/local-cut-studio/dist/cli.js", "--mcp"] } } }

Cline(VS Code 插件)则是在它的 MCP Marketplace 设置里填同样格式的 JSON。配置好之后,重启客户端,让 Agent 重新扫描工具列表。

有一个需要注意的点:命令必须写绝对路径。很多 Agent 客户端启动的进程环境和你终端里的 shell 环境不一样,用npx或者相对路径可能找不到依赖。我第一次配置失败,就是因为写的是npx local-cut-studio,Claude Desktop 启动后找不到这个命令。改成 node 加绝对路径脚本之后就稳定了。

3.3 工具清单设计:我暴露了哪 12 个核心工具给 Agent

整个 MCP Server 我暴露了 12 个核心工具,覆盖从创建项目到导出成片的完整链路。

工具名作用关键参数
create_project创建剪辑项目project_name, width, height, fps
list_projects查看本地所有项目无
get_project_state获取当前项目状态project_id
import_media导入本地素材project_id, file_path
add_clip将素材片段加入时间轴project_id, media_id, start, end, track
move_cursor画布光标左右移动project_id, direction, seconds
trim_clip裁剪指定片段project_id, clip_id, new_start, new_end
remove_clip删除指定片段project_id, clip_id
set_transition设置片段间转场效果project_id, clip_a, clip_b, type, duration
add_subtitle添加字幕project_id, text, start, end
apply_template应用画布模板project_id, template_name
export_video导出成片project_id, output_path, format

工具数量不是越多越好,而是要保证每个工具边界清晰、参数简单。我早期加过很多细分工具,比如 split_clip、duplicate_clip、adjust_volume,后来发现 Agent 经常混淆,调用成功率反而下降了。精简到 12 个之后,工具的语义一目了然,Agent 几乎不需要额外提示就能正确选用。这也算是一个“少即是多”的例子。

3.4 让 Agent 理解“先做什么后做什么”:工具描述里的依赖引导

Agent 调用工具并不是完全自由的,它的行为会受到工具描述和参数描述的强烈影响。想让 Agent 不犯错,关键是在描述中写清楚工具间的依赖关系和调用时机。

我举一个例子。add_clip 的参数描述里,我不仅写“媒体 ID”,还会加一句“通过 import_media 获取,导入成功后返回的 media_id”。“通过 xxx 获取”这个提示非常有用,Agent 会自动先调用 import_media,再调 add_clip。

再比如 export_video 的描述,我会写上“导出前请确保时间轴已有至少一个 clip,否则会返回 ERROR_EMPTY_TIMELINE”。Agent 读到这个描述后,会在导出前主动检查时间轴状态,如果为空,它会先添加素材再导出。

这种做法实际上是在给 Agent 画了一条隐形的流程引导,类似 libTV 的提示词格式约束。区别在于 libTV 的提示词格式是给用户背的,而我这里是直接写成机器可读的工具描述,Agent 每次调用前都会重新读取,所以不需要用户去记忆。

4. 从空画布到成片:Agent 全流程操控一个短视频项目

4.1 场景设定:让 Agent 完成一段 30 秒产品介绍视频

理论讲多了,还是得看一次真实的调用过程。我做了一个测试场景:给 Agent 一段自然语言指令,让它完成一个 30 秒的产品介绍视频。素材是本地三份 MP4 文件,分别是产品开箱、功能演示、用户评价。目标是让 Agent 自主完成导入、排列、裁剪、加字幕、导出。

我用的指令是:“帮我做一条 30 秒产品介绍视频,用本地素材目录 /Users/me/videos 下的三个文件,按开箱、功能、评价的顺序排列,展示 3D 打印机功能演示那段要保留完整,其他可以适当裁剪,结尾加字幕‘智能制造,从此简单’,导出成 mp4 到桌面。”

4.2 第一步:创建项目与导入素材

Agent 拿到指令后,首先调用 create_project。这一步通常没有任何偏差,因为描述里写清楚了返回 project_id。

实际返回结果:

{ "content": [ { "type": "text", "text": "{\"project_id\":\"proj_8f3k2a\",\"project_name\":\"3D打印机产品介绍\",\"status\":\"created\"}" } ] }

拿到 project_id 后,Agent 继续调用 import_media。它会读取本地的三份文件,逐个导入。在 import_media 的实现里,我做了文件格式校验,非视频文件会返回错误,这样 Agent 就能及时感知到路径不对。

{ "tool": "import_media", "arguments": { "project_id": "proj_8f3k2a", "file_path": "/Users/me/videos/unboxing.mp4" } }

返回结果里包含 media_id、时长、分辨率等元信息,Agent 会把这些信息记住并用于后续的 add_clip 调用。

4.3 第二步:画布左右移动与片段裁剪

素材导入完之后,Agent 开始按顺序向时间轴添加片段。这里涉及“画布左右移动方法”的实战含义:Agent 并不需要直接看到画面,它通过 move_cursor 工具来调整当前的焦点位置,然后决定在哪一个时间点插入或裁剪片段。

我按原始需求描述设计了一次有代表性的调用序列:

  1. add_clip 把开箱视频 0-10 秒加入轨道
  2. add_clip 把功能演示整段加入轨道
  3. move_cursor 向右移动 10 秒,找到功能演示段的起始位置
  4. trim_clip 把功能演示段裁剪到 10-22 秒
  5. add_clip 把用户评价视频加入轨道,位置在 22 秒之后
  6. move_cursor 向左移动 5 秒,定位到评价段开头,确认片段衔接

这套操作里,move_cursor 并不是必须的,但 Agent 在不确定时间轴位置时,会主动调用它来校准。我发现这其实是 Agent 的一个很聪明的行为:在没有可视化界面的情况下,它通过光标位置和时间轴状态来建立对项目的空间认知。

4.4 第三步:Agent 自主调用转场、字幕与导出

片段排列好了,Agent 下一步调用了 set_transition,在功能演示段和评价段之间添加了一个交叉溶解转场,时长 0.5 秒。

{ "tool": "set_transition", "arguments": { "project_id": "proj_8f3k2a", "clip_a": "clip_002", "clip_b": "clip_003", "type": "crossfade", "duration": 0.5 } }

转场设置完成后,Agent 调用 add_subtitle 在最后 3 秒添加字幕“智能制造,从此简单”。字幕的开始时间和结束时间,Agent 会根据时间轴总长度自动计算。这里我发现了一个值得注意的行为:Agent 会先调用 get_project_state 查看时间轴长度,再倒推字幕位置,而不是拍脑袋填一个数字。

最后一步是 export_video。Agent 指定输出路径为桌面,格式 mp4。导出过程会实时返回进度信息,Agent 能够在导出完成后拿到最终文件路径。

整个调用链路记录如下,总共 12 次工具调用,全程没有人工介入:

create_project import_media (unboxing.mp4) import_media (feature.mp4) import_media (review.mp4) add_clip (unboxing 0-10s) add_clip (feature 0-30s) move_cursor (right 10s) trim_clip (feature 10-22s) add_clip (review 0-8s) set_transition (clip_002 -> clip_003) add_subtitle ("智能制造,从此简单") export_video

4.5 中间失败与恢复:排查一次真实调用链路

虽然整体流程很顺,但实际跑的时候还是失败过一次。Agent 在导入素材时,开箱视频文件路径里有一个空格,它把路径写成"/Users/me/videos/unboxing.mp4",而我本地实际文件名是unboxing_final.mp4。这可能是因为 Agent 根据我的自然语言描述猜了文件名,而不是真的去目录里看。

import_media 返回了如下错误:

{ "isError": true, "content": [ { "type": "text", "text": "{\"errorCode\":\"ERROR_FILE_NOT_FOUND\",\"details\":\"File not found: /Users/me/videos/unboxing_final.mp4\"}" } ] }

Agent 收到错误后,立刻调用了 list_projects 之外的另一个工具 list_media_probe,重新扫描目录下真实可用的文件,然后修正路径再次导入。这让我确认了结构化错误返回的重要性:只有错误信息足够精确,Agent 才能进行自我纠偏。如果把错误包装成一串无结构的人类读文本,Agent 很可能要猜测下一步该干什么。

5. 踩坑实录:状态同步、路径注入、Agent 乱序调用这三大坑

5.1 状态同步:画布界面显示和实际项目状态不一致

第一个大坑是状态同步。最初版本里,画布界面维护自己的 UI 状态,剪辑内核维护项目状态,MCP Server 读写项目状态。看似清晰的隔离,实际上因为事件监听不到位,会出现“界面显示片段还在,实际已经被 Agent 删掉”的情况。

调试了很久,最后我把项目状态设计成了唯一数据源,画布界面不再保存任何剪辑相关的数据,只订阅状态变更事件。每次 Agent 调用工具导致状态变化,都会广播一个状态变更事件,画布收到后重新拉取全量状态渲染。

这个决策看起来很基础,但能避免一类很隐蔽的问题:Agent 拿到旧状态、基于旧状态继续操作,最后把新改动覆盖掉。现在所有读写都走状态机,通过版本号控制并发冲突,Agent 的操作就变得可预期了。

5.2 文件路径注入与权限边界

Agent 可以调用 import_media 导入任意路径,如果完全不校验,Agent 被恶意提示词诱导时,有可能读取系统上不该读的文件。虽然 Agent 本体有自己的安全机制,但工具端也应该有自己的边界。

我做了两层防护。第一层是路径校验:import_media 只允许导入媒体格式后缀的文件,并且路径必须处在配置的媒体根目录下。第二层是权限注入:MCP Server 启动时可以指定--allow-dir,只有该目录下的文件可以被访问。

local-cut-studio --mcp --allow-dir /Users/me/videos

这个设计既是安全措施,也是提示词工程的一部分。Agent 看到 import_media 的描述里写着“只支持 /Users/me/videos 目录下的文件”,就会主动把用户的路径需求翻译成合法路径,而不是绕过去找别的路径。

5.3 Agent 乱序调用:如何用结构化错误让 Agent 自我纠偏

Agent 乱序调用是绕不开的问题。最典型的例子是,Agent 还没有导入任何素材,就直接调用 set_transition,结果返回了“找不到片段”的错误。原因是 Agent 根据用户指令里的“加转场”优先做了转场操作,忘了时间轴上还没有片段。

遇到这种情况,不要指望 Agent 一次就做对,而是要让错误信息足够精确地指出“你漏掉了什么”。我实现的 set_transition 在找不到片段时,会返回如下错误:

{ "errorCode": "ERROR_CLIP_NOT_FOUND", "details": "Timeline has 0 clips. Need at least 2 clips to set transition.", "hint": "Try calling import_media then add_clip before set_transition." }

hint 字段是关键。它不是给用户看的,而是给 Agent 看的。Agent 拿到 hint 后会自动调整顺序,重新执行导入、添加、设置转场。我把这个思路应用到了所有可能因前置条件不满足而报错的工具上。

这个方案不能说百分百完美,但实测下来 Agent 的自我纠偏成功率至少提升了一半。如果你也在做 MCP 工具,强烈建议所有错误返回都带上 hint。

5.4 性能优化:大视频素材带来的卡顿和超时

本地剪辑最现实的性能问题是 FFmpeg 处理大文件时的耗时。Agent 调用 export_video 后,如果直接同步等待导出完成,MCP Server 的进程会被长时间占住。如果 Agent 客户端还有超时限制,就会误报失败。

我的解决方案是引入异步任务机制。export_video 立即返回一个 task_id,后续 Agent 可以调用 task_status 查询进度。这样既不会阻塞服务进程,也让 Agent 能够更优雅地等待任务完成。渲染队列也做了并发控制,同一时间只跑一个导出任务,避免多个任务争抢 CPU 导致导出失败。

为了提高缩略图和预览图的响应速度,我给导入的素材做了 probe 缓存。第一次导入时解析媒体元信息,存到本地 SQLite 里,后续就不用反复执行 FFprobe。这个优化对于几十个素材的项目效果非常明显,导入速度能快一倍以上。

还有一个容易被忽视的坑:内存。多个大视频同时被解码时,内存占用可能轻松超过 2GB。我在测试中把渲染队列改为串行之后,内存稳定在 800MB 以内,体验提升了一个档次。

6. 项目的边界与后续扩展:它能做什么、不能做什么

这个项目目前能覆盖的,基本就是单轨时间轴的剪辑流程:导入素材、按时间轴排列、裁剪、转场、字幕、导出。对于日常短视频、产品介绍、口播视频的自动化生成来说,已经够用了。我实际把这套东西跑在每周固定生成工作复盘视频的流程里,效果稳定,省下了不少手动剪辑时间。

但我也不会回避它的不足。多轨视频叠加、画中画、关键帧动画、音频 EQ 调音这类复杂功能,当前版本并没有实现。如果你拿它当 Premiere 的替代品,肯定会失望。它的定位不是“又一个大而全的剪辑软件”,而是“一个能被 Agent 顺畅操控的本地剪辑工具”。

后续有几个方向我认为价值比较大:第一是支持多轨时间轴,让 Agent 能编排画中画和字幕轨道;第二是把渲染器插件化,允许接入不同的渲染后端;第三是做一个画布实时预览窗口的 WebSocket 推流,让用户能看到 Agent 剪辑的每一帧变化。

如果你想参与开源贡献,可以从这三个方向选一个入手。代码结构和工具注册方式在仓库 README 里有说明,MCP Server 部分很好定位,因为所有工具入口都集中在一个 tools 目录下,新增一个工具只需要写一个注册函数加一个处理函数即可。

最后分享一个小经验:当你设计 Agent 可操控的工具时,要像设计 API 一样克制。每一个工具都必须是不可再分的原子操作,并且描述里必须写清前置条件和返回值。Agent 是一个极其聪明但偶尔短路的使用者,你给它的信息越结构化,它发挥得就越稳定。这个项目的核心价值不在于我写了多少行代码,而在于把“AI 自动剪视频”从 demo 变成了一个可以跑在本地、可以被任意 MCP 客户端调用的工程化方案。你可以把它理解成一个基础设施,至于 Agent 能用它拍出什么样的片子,那就是上层应用的事了。

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

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

立即咨询