QAgent 工程实践:构建受控自主 Web Agent 的关键设计与实现

QAgent 工程实践:构建受控自主 Web Agent 的关键设计与实现

本文记录 QAgent 这个面向课程资料整理场景的轻量级 Web Agent 的工程实现过程,包括核心设计取舍、若干关键模块的落地方式,以及在实际运行中遇到的工程问题和应对思路。

一、项目背景

课程资料整理是高频但重复度很高的工作:搜索相关资料 → 整理到笔记 → 输出可交付物(讲义、PPT、PDF)。这一链路虽然单步都不复杂,但拼接成本持续存在。

QAgent 的目标,是把这条链路以一个连续对话的 Web Agent 形式跑通:用户用自然语言描述需求,Agent 自主判断是否需要检索资料、是否需要生成文件,最终给出可用的回答或可下载的产物。

在选型上,参考了 LangChain 的工具注册与结构化输出思想,也参考了 LangGraph 的状态化流程与可观测执行轨迹,但项目最终选择了自研轻量实现,原因有两个:

  1. 课程项目规模可控,框架的抽象成本高于收益;
  2. 自研实现更便于讲解与演示,部署依赖也最小。

二、整体架构

QAgent 的核心循环遵循"读取上下文 → 模型决策 → 工具执行 → 写入执行轨迹"的模式:

用户输入
  │
  ▼
保存用户消息
  │
  ▼
受控自主 Agent 循环
  │   ├─ 读取长期记忆 / 最近消息 / 已生成文件
  │   ├─ 模型输出 JSON 决策:final 或工具 action
  │   ├─ 后端校验 action / args 白名单
  │   ├─ 执行工具并记录 observation
  │   └─ 继续循环,直到 final 或达到步数上限
  ▼
写入结果与会话状态

模块划分如下:

模块 职责
qagent.py HTTP 路由入口
agent_core.py 会话启动、Agent 调度、记忆压缩
autonomous_agent.py 受控自主 Agent 工具循环
tools.py 工具注册表与调用封装
web_research.py 搜索 API 与 URL 摘要读取
artifact_builder.py Markdown / HTML / PPTX / PDF 生成
llm_client.py LLM 调用与 JSON 解析
prompts.py 提示词与 Skill 加载
storage.py SQLite 会话、消息、步骤、工具日志

三、关键设计决策

3.1 受控的自主 Agent 循环

Agent 的"自主性"是有边界的:模型输出结构化 JSON,后端只执行白名单内的动作,并在步数上限内终止。当前允许的动作集合是固定的:

ALLOWED_ACTIONS = {
    "read_conversation",
    "read_artifact",
    "search_web",
    "read_url",
    "summarize_text",
    "generate_files",
    "final",
}
ALLOWED_FORMATS = {"md", "html", "pptx", "pdf"}

任意超出白名单的动作、任意非受控文件格式,都会被后端拒绝。这一约束带来了两个直接好处:

  • 可观测性:执行轨迹以 plan / action / observation / result 四段形式直接渲染到前端,问题定位可以精确到具体步骤;
  • 安全性:模型不会越权读取本地文件或执行任意命令,能力面收敛在受控工具集合内。

3.2 Skill 作为提示词能力包

项目内的 Skill 不是可执行代码,而是按目录组织的 Markdown 提示词包:

skills/
  agent_skills/SKILL.md
  concept_research/SKILL.md
  course_report/SKILL.md
  general_research/SKILL.md
  ppt_maker/SKILL.md

后端根据任务类型和目标输出格式,挑选对应的 SKILL.md 拼入 system prompt。这与 Codex / Claude Code 中"渐进式披露 Skill"的做法类似:能力按需加载,system prompt 体量可控,行为也更容易聚焦。

3.3 文件生成的二次确认

一个容易忽略的问题是:模型在对话中提到 "PPT skill" 不等于用户要生成 PPT。直接根据关键词触发文件生成,会出现"对话过程中突然冒出来一个文件"的体验。

QAgent 的处理策略是:只有在用户显式勾选输出格式或模型显式声明 generate_files 时,才进入文件生成流程。如果模型已经选择 generate_files 但漏填格式,后端会再调用一次轻量补全请求,仅判断"是否真的要生成文件"以及"应为哪几种格式"。文件生成器本身不再依赖关键词猜测格式,只接受 Agent 已确认的格式并做白名单校验。

四、若干工程问题与应对

4.1 模型输出 JSON 的容错

理论上,结构化输出工具或严格提示可以让模型稳定输出 JSON;实际上,模型仍可能输出带 markdown fence、尾逗号或额外说明文本的"半脏 JSON"。

处理方式是两级兜底:

  1. 一次宽容解析:先剥离常见的 fence 包裹,再尝试 json.loads;
  2. 解析失败时调用一次专门的 JSON 修复请求,让模型把脏文本处理成干净 JSON。

两步配合后,结构化决策的失败率显著下降。

4.2 长上下文会话的记忆压缩

即便底层模型支持较长上下文,几十轮对话之后仍会出现"早期事实漂移"的问题。QAgent 引入了一个简洁的会话记忆压缩机制:

  • 最近的 N 条消息原文保留;
  • 之前更早的消息合并进一份长期摘要;
  • 下一次对话时把摘要塞回 system prompt。

触发条件同时考虑消息数与字符数阈值。摘要内容要求保留用户真实目标、偏好、关键约定与未解决问题,删除寒暄、重复内容与过时的执行细节,不引入新事实、不替用户做新的推断。

4.3 PPTX 的自适应页数与版式

PPTX 生成最容易翻车的地方是"密度":每页放多少文字、是否需要拆成续页,单纯按大纲条数切分几乎一定出问题。

QAgent 的做法是按内容密度自适应:

density = sum(len(str(item or "")) for item in
              [slide.get("claim"), slide.get("example")] + bullets + details)
too_dense = len(bullets) >= 6 and len(details) >= 4 and density > 620
very_dense = density > 900 and (len(bullets) + len(details)) >= 7

密度过高的页面会拆成续页,版式(标题居中、双栏、要点+细节)也按密度切换:低密度页面给大标题与充足留白,高密度页面切到紧凑双栏。这一处理避免了文字溢出文本框或被压缩得过小的问题。

4.4 后台任务的状态收敛

模型调用丢到工作线程中执行之后,"任务看起来已经结束但状态没写回"是高发问题。常见场景包括:

  • 用户中途删除会话,后台任务仍在执行并尝试落库;
  • 线程异常退出,session 状态停留在 running / queued;
  • 服务重启后旧任务仍处于运行中。

应对策略是状态收敛:

  • 删会话时同时取消对应线程,并清理尚未落库的文件;
  • 线程内增加兜底写入完成状态的 finally 块,确保异常退出也会写回 failed 状态;
  • 服务启动时把状态为 running 的历史会话统一标记为 interrupted。

收敛之后,前端不再出现"长时间转圈无响应"的情况。

4.5 文件去重

同一轮任务中,模型可能在不同步骤重复触发文件生成(例如"先生成一份、再生成一份更详细的"),导致前端出现多条相同下载项。处理方式是按格式去重:同一会话同一格式只保留最终版本,避免冗余下载。

五、模块协作示例

以"帮我整理 Transformer 自注意力机制的资料,做一份讲义 PPT"为例,典型的执行轨迹如下:

  1. plan:根据用户请求判断需要先检索资料,再生成 PPTX;
  2. action search_web:检索 Transformer 自注意力机制 通俗讲解,得到若干标题、URL、摘要;
  3. action read_url:对关键来源做正文摘要,提取核心论据;
  4. action summarize_text:把检索结果压缩为可用素材;
  5. action generate_files:按确认后的 pptx 格式输出文件;
  6. final:返回可下载链接与简要交付说明。

每一步都会作为 step 写入存储层,前端按时间顺序渲染。

六、当前能力与边界

为避免对模型能力的"过度承诺",QAgent 在产品说明中明确划定了边界:

  • 提供:Web 对话、受控工具调用、文件生成、会话管理;
  • 不提供:任意本地文件读取、代码执行沙箱、用户上传文件解析。

这一边界在提示词与后端校验两侧共同维护,确保模型在"做不到"时不会编造能力。

七、后续工作

短期计划:

  • 拆分为多 Agent 协同(检索 Agent + 写作 Agent + 排版 Agent);
  • 引入人类确认节点,对高风险动作(删除会话、覆盖已有文件)做显式确认;
  • 长任务的断点恢复与状态持久化。

中期方向:

  • 当任务复杂度超出当前自研实现的承载范围时,将 autonomous_agent.py 升级为 LangGraph 状态机,借助其内置的检查点与人机协作机制承载更复杂的工作流。

八、小结

整个项目的工程量不大,但实际推进中的复杂度大多集中在"系统层"——循环设计、边界约束、状态收敛、容错兜底——而不在"模型层"。这或许是一个常见的认知校准:让 LLM 真正去完成一个端到端任务,可靠性更多来自系统设计,而非模型本身的智能水平。


参考

  • LangChain: Tool Registration and Structured Output
  • LangGraph: Stateful Agent Orchestration
  • Tavily / Brave Search / SerpAPI: 搜索 API 服务
posted @ 2026-07-09 17:15  知离叶  阅读(18)  评论(0)    收藏  举报