QAgent 工程实践:构建受控自主 Web Agent 的关键设计与实现
QAgent 工程实践:构建受控自主 Web Agent 的关键设计与实现
本文记录 QAgent 这个面向课程资料整理场景的轻量级 Web Agent 的工程实现过程,包括核心设计取舍、若干关键模块的落地方式,以及在实际运行中遇到的工程问题和应对思路。
一、项目背景
课程资料整理是高频但重复度很高的工作:搜索相关资料 → 整理到笔记 → 输出可交付物(讲义、PPT、PDF)。这一链路虽然单步都不复杂,但拼接成本持续存在。
QAgent 的目标,是把这条链路以一个连续对话的 Web Agent 形式跑通:用户用自然语言描述需求,Agent 自主判断是否需要检索资料、是否需要生成文件,最终给出可用的回答或可下载的产物。
在选型上,参考了 LangChain 的工具注册与结构化输出思想,也参考了 LangGraph 的状态化流程与可观测执行轨迹,但项目最终选择了自研轻量实现,原因有两个:
- 课程项目规模可控,框架的抽象成本高于收益;
- 自研实现更便于讲解与演示,部署依赖也最小。
二、整体架构
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"。
处理方式是两级兜底:
- 一次宽容解析:先剥离常见的 fence 包裹,再尝试
json.loads; - 解析失败时调用一次专门的 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"为例,典型的执行轨迹如下:
- plan:根据用户请求判断需要先检索资料,再生成 PPTX;
- action
search_web:检索Transformer 自注意力机制 通俗讲解,得到若干标题、URL、摘要; - action
read_url:对关键来源做正文摘要,提取核心论据; - action
summarize_text:把检索结果压缩为可用素材; - action
generate_files:按确认后的pptx格式输出文件; - 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 服务

浙公网安备 33010602011771号