哈喽,大家好,
我是阿星!
前几天搞了个自动切片剪辑工具,我用Codex20分钟手搓直播切片软件
这套模式的优点很明显:稳定、清楚、容易调试。
但它还不是严格意义上的 Agent
为了给大家讲一下agent到底啥结构,我把它改成了agent
现在效果如下:
用户上传视频、说出剪辑目标后,系统会把这次剪辑当成一个任务来处理:
当前目标是什么、视频是否已上传、候选生成到哪一步、哪些片段被选中、导出是否已经确认,都会进入同一条任务时间线。
然后它就完成了一系列剪辑工作
点击「勾选导出片段」, 即可导出相关片段
后续用户再说“导出第 1 个”“打开输出目录”“停止任务”,Agent 都能基于当前任务继续处理。
然后
你就可以点击在finder里打开
结构图
所以这次改造的目标是把控制逻辑从 UI 表单改成 Agent Loop。
具体的Agent设计长这样
一、核心变化:从“点按钮”变成“推进任务”
这个Agent 版是系统围绕一个任务持续对话、调用工具、读取真实产物,
并在关键节点请求用户决策。
现在是 Agent 根据当前任务状态、候选片段、产物信息和用户指令,
判断下一步应该是读状态、生成候选、请求确认,还是导出片段。
二、模型负责决策,工具负责执行
现在的主入口是 Agent 页面。用户消息先进入统一的 Agent 通道,再由模型判断下一步应该做什么。
模型不会直接操作文件,也不会随意运行命令。它只能调用系统注册好的工具,比如生成候选、读取状态、查看产物、请求导出确认、正式渲染、停止任务、打开输出目录。
这点很关键:Agent 的“自主”不是没有边界,而是在明确边界内自己选择下一步。业务动作仍然走原来的稳定剪辑流水线,Agent 只负责理解上下文、选择工具、把工具结果带回对话。
三、上下文压缩:只把有用信息交给模型
视频剪辑任务里,字幕、日志、候选池都可能很长。如果每轮都把完整内容塞给模型,成本高,也容易干扰判断。
所以这一版做了静态上下文压缩。
用户原话、最近对话、已选候选和关键状态优先保留;较新的长工具结果会做 Snip,也就是保留开头和结尾,中间插入省略标记;更旧的工具结果会做 Prune,也就是把原始长内容替换成占位符,只保留阶段、产物路径和回取线索;超过最近窗口的历史会被整理成任务摘要。
这样模型默认看到的是干净的任务状态和关键证据,需要细节时再通过工具读取完整字幕、日志或候选详情。
四、导出和停止必须确认
视频导出和停止任务都属于有副作用的动作。它们会占用资源、生成文件,或者中断当前任务。
所以 Agent 可以建议导出,也可以请求确认,但不能在用户没有明确确认的情况下直接渲染。停止任务也是同样逻辑。
这类内容生产工具里,一个比较稳的原则是:AI 负责整理候选,人负责最终判断,系统负责稳定执行。
具体的剪辑工作流长这样
源码对应关系:
Part 1:任务时间线
对应文件:scripts/agent_core.py
class MessageStore:
# 记录用户输入:这是任务目标和后续确认的来源。
defappend_user(self, thread_id: str, content: str) -> dict: ...
# 记录 Agent 给用户的回复。
defappend_assistant(self, thread_id: str, content: str) -> dict: ...
# 记录模型决定调用哪个工具,以及传了什么参数。
defappend_tool_call(self, thread_id: str, call_id: str, name: str, arguments: dict) -> dict: ...
# 记录工具执行结果。下一轮模型会基于这些结果继续判断。
defappend_tool_result(self, thread_id: str, call_id: str, result: ToolResult) -> dict: ...
这一层把一次剪辑任务变成可追溯的时间线:用户说了什么、Agent 调了什么工具、工具返回了什么结果,都会按顺序落到本地 JSONL。
Part 2:模型决策与工具执行
对应文件:scripts/agent_core.py
class AgentLoop:
def run(self, thread_id: str, user_message: str, *, context: dict | None = None) -> dict:
# 先把用户这轮输入写进同一条任务时间线。
self.store.append_user(thread_id, user_message)
# 给模型前,先做上下文压缩,避免字幕、日志、候选池太长。
messages = self.context_compact.compact(self.store.load(thread_id), context=context)
# 模型只负责做决定:回复用户,或者调用某个已注册工具。
decision = self.model(messages, self.tools.schemas(), context)
...
Agent 每一轮都会读取任务上下文,让模型判断下一步。如果模型选择工具调用,就通过注册表执行;工具结果再写回时间线,进入下一轮判断。
Part 3:受控工具集合
对应文件:scripts/ui_server.py
def build_chat_tool_registry(runs_root: Path = RUNS_DIR) -> tuple[agent_core.ToolRegistry, dict]:
# ToolRegistry 是工具白名单。模型只能调用这里注册过的工具。
registry = agent_core.ToolRegistry()
# 读任务状态:当前阶段、进度、错误、候选摘要。
registry.register("read_status", read_status, ...)
# 按需读取产物:候选详情、warning、产物路径等。
registry.register("read_artifacts", read_artifacts, ...)
# 导出前先请求用户确认候选 ID。
registry.register("request_render_confirmation", request_render_confirmation, ...)
# 用户确认后,才真正启动渲染。
registry.register("render_selected", render_selected, ...)
模型不能直接操作文件或执行命令,只能调用系统注册好的业务工具。这是 Agent 可控的关键。
Part 4:导出和停止确认
对应文件:scripts/ui_server.py
def require_sensitive_tool_confirmation(name: str, arguments: dict) -> agent_core.ToolResult | None:
# 导出视频会生成文件、占用资源,所以必须显式确认。
if name == "render_selected" and not bool(arguments.get("confirmed")):
return agent_core.ToolResult(ok=False, stage="permission_denied", message="导出前需要用户明确确认候选 ID。")
# 停止任务会中断后台流程,也必须显式确认。
if name == "stop_job" and not bool(arguments.get("confirmed")):
return agent_core.ToolResult(ok=False, stage="permission_denied", message="停止任务前需要用户明确确认。")
导出和停止都是有副作用动作,所以模型可以建议,但不能绕过用户确认。
Part 5:上下文压缩策略
对应文件:scripts/agent_core.py
# 示意代码:工具结果根据“距离最新消息的远近”选择压缩方式。
should_prune = distance_from_latest >= self.prune_after_messages
if should_prune:
# 旧工具结果:不再保留原始长 message,只留阶段占位符。
compacted["message"] = self._prune_message(result)
compacted["compaction"] = "prune"
else:
# 新近工具结果:保留 message 的开头和结尾,中间省略。
compacted["message"] = self._snip_text(result["message"])
compacted["compaction"] = "snip"
当前是可用的静态压缩;标准做法会进一步做 token budget、结构化 recall、自动回取和更强的程序层校验。
ok,我是阿星
更多AI应用
我们下期再见!