AI Agent 真正开始前,先把上下文工程做好

8 阅读16分钟

上一篇文章,我写了这段时间为什么停更:不是不做 AI Agent 了,而是带团队把之前讲过的概念放进了一个真实业务系统里验证。

这个系统是一个 AI Agent 小说创作系统

第一篇更多是总览:为什么不能只做一个“AI 续写按钮”,为什么要做 Agent 任务编排,为什么要有 AgentTaskAgentStep,以及它和流程引擎到底有什么区别。

这篇开始进入更具体的工程问题。

我想先讲上下文。

因为做完这一轮项目后,我越来越觉得:

AI Agent 的第一道工程边界,不是工具调用,也不是任务规划,而是上下文。

如果上下文不可信、不隔离、不可追溯,后面所有 Agent 能力都会变得很虚。

模型看起来在规划,实际上可能是在错误信息上规划。

模型看起来在调用工具,实际上可能是在缺少关键事实的情况下猜。

模型看起来在连续写作,实际上可能已经把人物状态、世界设定和章节目标写偏了。

所以这一篇,我想结合当前项目代码,聊聊我为什么把上下文工程放在 Agent 落地的前面。

先说结论

在这个系统里,我没有把上下文当成一段 prompt 字符串。

而是把它拆成了几层:

事实层:作品、角色、世界观、大纲、章节、规则、风格、记忆
组装层:按不同任务构建上下文
工具层:Agent 可以按需读取上下文窗口
审计层:上下文来源、步骤输出、任务状态可记录
演进层:后续接入作品圣经、章节计划、连续性审核

这样做的目的不是让 prompt 看起来更复杂。

真正目标是:

让 Agent 每一次执行任务时,都知道自己基于哪些事实在做判断。

这件事非常重要。

因为 Agent 和普通 AI 调用最大的区别之一,就是它不只是生成一段文本,而是要在业务流程里持续做判断。

判断要不要修复,判断用什么技能,判断这一章怎么承接上一章,判断是否偏离大纲,判断哪些内容应该沉淀成记忆。

这些判断都依赖上下文。

上下文错了,Agent 越积极,系统越危险。

为什么不能继续手写 prompt

很多 AI 应用一开始都会这么写:

你是一个小说作者。
请根据下面的角色、世界观、大纲,写第 10 章。
角色:...
世界观:...
大纲:...

Demo 阶段这样没问题。

但系统一旦进入真实业务,就会很快遇到几个问题。

第一,数据来源会越来越多。

小说创作不是只需要角色和大纲,还会涉及:

作品简介
笔名风格
角色关系
世界设定
章节历史
上一章结尾
写作规则
禁词
风格锁
写作技能
长期记忆
伏笔状态

第二,不同任务需要的上下文不一样。

正文生成需要写作风格、上一章结尾和章节方向。

编辑审核需要当前正文、规则和质量标准。

剧情规划需要作品设定、大纲窗口、历史章节和可用技能。

记忆提取需要最终正文和章节信息。

如果所有任务都用一个大 prompt,最后一定会变成一坨很难维护的字符串。

第三,多作品隔离会变成隐患。

一个系统里如果支持多个作品,就不能让上下文靠前端临时拼接。

否则很容易出现:

作品 A 的角色混进作品 B
全局规则覆盖了作品专属规则
旧章节状态没有更新
用户切换作品后仍然沿用旧上下文

所以我在项目里做的第一步,就是把上下文从“手写 prompt”里抽出来,变成后端服务。

第一层:动态 system prompt

当前系统里有一个 writing_context.py

它的职责很明确:

从数据库读取作品专属信息,构建 AI 写作 system prompt。

这里读取的不是一两个字段,而是一组创作事实:

Novel
Character
WorldSetting
WritingRule
WritingStyle
OutlineItem
Skill

这意味着 Agent 写作时,不是拿一段静态 prompt 去跑,而是根据 novel_id 动态构建上下文。

入口函数大概长这样:

async def build_system_prompt(
    novel_id: int,
    db: AsyncSession,
    *,
    include_craft: bool = True,
    include_format: bool = True,
    include_characters: bool = True,
    include_world: bool = True,
    include_style: bool = True,
    include_arcs: bool = True,
    inject_skills: list[int] | None = None,
    extra_context: str = "",
) -> str:
    """构建完整的 AI 写作 system prompt,所有数据来自数据库。"""

这个签名里有两个点很重要。

第一个是 novel_id

上下文不是由前端传一坨字符串进来,而是后端根据作品 ID 去数据库取事实。

第二个是这些 include_xxx 开关。

不同任务可以复用同一个构建器,但按需关闭某些上下文。比如有的任务只需要角色和世界观,有的任务需要完整写作规则。

代码里的思路大概是这样:

读取作品和笔名
        ↓
注入风格锁
        ↓
加载写作铁律和格式铁律
        ↓
加载禁词
        ↓
加载世界观
        ↓
加载主要角色
        ↓
加载作品简介
        ↓
加载后续大纲
        ↓
按需注入写作技能
        ↓
生成 system prompt

这里有几个架构点值得单独说。

第一个是全局默认和作品覆盖。

比如写作规则会先查作品专属规则,如果没有,再查全局规则。

代码里是这个逻辑:

async def _load_rules(db: AsyncSession, novel_id: int, rule_type: str) -> list[dict]:
    result = await db.execute(
        select(WritingRule).where(
            WritingRule.novel_id == novel_id,
            WritingRule.rule_type == rule_type,
            WritingRule.enabled == 1,
        )
    )
    novel_rules = result.scalars().all()
    if novel_rules:
        return [
            {"pattern": r.pattern, "fix_hint": r.fix_hint or "", "severity": r.severity}
            for r in novel_rules
        ]

    result2 = await db.execute(
        select(WritingRule).where(
            WritingRule.novel_id.is_(None),
            WritingRule.rule_type == rule_type,
            WritingRule.enabled == 1,
        )
    )
    global_rules = result2.scalars().all()
    return [
        {"pattern": r.pattern, "fix_hint": r.fix_hint or "", "severity": r.severity}
        for r in global_rules
    ]

这段代码背后的设计是:作品规则优先,全局规则兜底

这样既能让系统有默认能力,又能让每个作品保留自己的差异。

第二个是风格锁。

WritingStyle 不是简单标签,而是可以带 system_prompt

这意味着风格不是散落在用户输入里的形容词,而是可以被固化、复用、注入到生成上下文里。

第三个是写作技能注入。

技能内容支持变量,比如 {{主角名}}{{作品名}}{{当前境界}}

系统会从作品上下文里解析默认值,再替换进技能模板。

变量解析也不是靠用户每次手填,而是从作品数据里推导:

SKILL_VAR_DEFAULTS = {
    "主角名": lambda novel_id, db: _first_char_name(novel_id, db),
    "主角": lambda novel_id, db: _first_char_name(novel_id, db),
    "作品名": lambda novel_id, db: _novel_title(novel_id, db),
    "当前境界": lambda novel_id, db: _first_char_level(novel_id, db),
}

这一步看起来小,但很关键。

因为它让“提示词技巧”从一次性的 prompt,变成了可复用、可挂载、可统计使用次数的业务能力。

第二层:章节级上下文

除了 system prompt,系统里还有一个更偏任务上下文的 context_builder.py

它不是构建全局写作人格,而是为某个 AI 工具提供当前章节附近的信息。

比如:

作品信息
主要角色
大纲弧线
当前章节
周边章节
上一章结尾

尤其是“上一章结尾”这一点,我觉得非常重要。

连续写作最怕什么?

不是文笔差一点,而是断。

上一章最后角色刚做了一个决定,下一章开头突然换了场景。

上一章刚埋下冲突,下一章完全忘了。

上一章还在战斗,下一章已经开始总结人生。

所以系统里会专门把上一章结尾截出来,作为续写入口。

代码里会按目标章节取相邻章节,并且专门追加上一章结尾:

if chapter_number and chapter_number > 1:
    prev_row = await db.execute(
        select(Chapter).where(
            Chapter.novel_id == novel_id,
            Chapter.chapter_number == chapter_number - 1,
        )
    )
    prev = prev_row.scalar_one_or_none()
    if prev and prev.content:
        tail = prev.content[-500:] if len(prev.content) > 500 else prev.content
        parts.append(f"【上一章结尾(从这接着写)】\n...{tail}")

这就是上下文工程和普通 prompt 的区别。

普通 prompt 可能会说:

请保持剧情连续。

上下文工程会直接提供:

上一章最后 500 字是什么
当前目标章节是什么
周边章节摘要是什么
大纲接下来要推进什么

前者是希望模型记得连续。

后者是给模型连续所需的事实。

第三层:Agent 工具化读取上下文

如果只是把上下文一次性拼进 prompt,这仍然不够 Agent。

因为 Agent 做规划时,可能不是所有信息一开始都需要。

比如它先看用户目标,发现本章是战斗高潮,那它可能需要查技能。

如果发现本章靠近大纲节点,它可能需要查附近大纲窗口。

如果需要确认人物状态,就需要读作品角色和世界设定。

所以当前系统里新增了 agent_tools.py

它给章节规划 Agent 暴露了一组只读工具:

get_novel_context
get_chapter_context
get_outline_window
search_writing_skills
get_style_rules

这些工具有一个共同点:

它们只读,不直接修改业务数据。

工具 schema 的代码大概是这样:

def chapter_planning_tool_schemas() -> list[dict[str, Any]]:
    return [
        {
            "type": "function",
            "function": {
                "name": "get_novel_context",
                "description": "读取当前作品的基础信息、角色和世界设定摘要。",
                "parameters": {
                    "type": "object",
                    "properties": {},
                    "additionalProperties": False,
                },
            },
        },
        {
            "type": "function",
            "function": {
                "name": "get_outline_window",
                "description": "读取目标章节附近的大纲窗口。",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "window": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 6,
                        },
                    },
                    "additionalProperties": False,
                },
            },
        },
    ]

这里我刻意省略了其他工具,只保留两个代表性的。

重点不是工具数量,而是工具边界:它们只负责读取事实。

这是我刻意保留的边界。

Agent 可以读取作品上下文,可以读取前几章结尾,可以读取大纲窗口,可以搜索写作技能,可以读取风格和规则。

但它不能直接改角色、改世界观、改大纲。

为什么?

因为规划阶段最重要的是“理解”,不是“写入”。

如果一开始就让 Agent 有太多写权限,系统会很难控制。

所以当前工具层的定位是:

让 Agent 按需获取事实
不让 Agent 静默修改事实
所有可变更内容后续走提案或审核

这也是 Agent 落地里很重要的工程纪律。

不是模型能调用工具,就什么工具都给它。

工具权限应该从业务风险倒推。

工具处理器也很克制,只根据工具名分发到只读查询:

def build_chapter_planning_tool_handler(
    db: AsyncSession,
    *,
    novel_id: int,
    target_chapter: int,
):
    async def handle(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
        if name == "get_novel_context":
            return await _get_novel_context(db, novel_id)
        if name == "get_chapter_context":
            return await _get_chapter_context(
                db, novel_id, target_chapter, int(arguments.get("limit") or 3)
            )
        if name == "get_outline_window":
            return await _get_outline_window(
                db, novel_id, target_chapter, int(arguments.get("window") or 3)
            )
        return {"error": f"未知工具: {name}"}

    return handle

第四层:章节规划不是拍脑袋

agent_orchestrator.py 里,write_chapter_loop 已经不只是加载上下文后直接生成正文。

它现在多了一个步骤:

chapter_plan

完整步骤大概是:

load_context
chapter_plan
generate_draft
save_draft
review_chapter
fix_chapter
extract_memory
finish_report

这一步的意义是:生成正文之前,先让 Agent 形成“本章作战计划”。

步骤定义在代码里是明确持久化的:

STEP_DEFINITIONS = [
    ("load_context", "加载上下文"),
    ("chapter_plan", "制定章节计划"),
    ("generate_draft", "生成章节草稿"),
    ("save_draft", "保存草稿"),
    ("review_chapter", "编辑审核"),
    ("fix_chapter", "自动修复"),
    ("extract_memory", "提取记忆"),
    ("finish_report", "生成报告"),
]

这不是为了好看,而是为了让每一步都能被记录、恢复和展示。

规划时,它会拿到:

用户方向
上一章上下文
附近大纲
可用技能
可调用工具列表

然后通过 chat_json_with_tools 让模型按需调用工具,最终输出结构化 JSON。

调用点大概是这样:

data = await chat_json_with_tools(
    "你是小说章节策划。只输出 JSON。",
    prompt,
    tools=chapter_planning_tool_schemas(),
    tool_handler=build_chapter_planning_tool_handler(
        db,
        novel_id=task.novel_id,
        target_chapter=task.target_chapter,
    ),
    temperature=0.4,
    max_tokens=2000,
    timeout=120,
    task="planning",
    novel_id=task.novel_id,
)

这里的核心不是“用了 function calling”,而是:

模型负责判断需要哪些信息
工具负责返回受控事实
编排层负责保存计划结果
后续生成按计划继续执行

这个结构化计划里包括:

goal:本章目标
continuity:承接上一章的点
conflict:主要冲突
pacing:节奏安排
hook:章末钩子
skills:建议挂载的写作技能
direction:正文生成方向

这比直接让模型“写第 N 章”稳很多。

因为它把一次生成拆成了两步:

先规划:这一章应该怎么写
再生成:按计划写正文

更重要的是,规划结果会写入 AgentStep

这意味着后续任务失败、重试、前端展示时,都能看到 Agent 当时为什么这么写。

这就是可观察性。

第五层:上下文还要参与技能选择

系统里还有一个细节:写作技能不是完全靠用户手动选。

Agent 会根据任务输入和上下文自动推荐技能。

推荐时会把这些内容拼成查询文本:

用户 prompt
章节方向
任务类型
作品题材
作品简介
前两章结尾
附近大纲

代码里可以看到,这个查询文本不是只看用户输入,还会读作品题材、简介、前两章结尾和附近大纲:

def _build_skill_query_text(task: AgentTask, context: dict[str, Any] | None = None) -> str:
    input_data = task.input or {}
    parts = [
        str(input_data.get("prompt") or ""),
        str(input_data.get("direction") or ""),
        str(input_data.get("current_stage") or ""),
        task.task_type or "",
    ]
    if context:
        novel = context.get("novel")
        if novel:
            parts.extend([
                getattr(novel, "genre", "") or "",
                getattr(novel, "synopsis", "") or "",
            ])
        for chapter in context.get("previous_chapters", [])[-2:]:
            parts.append((chapter.content or "")[-600:])
        for outline in context.get("outlines", [])[:5]:
            parts.extend([
                outline.title or "",
                outline.core_events or "",
                outline.payoff_points or "",
            ])
    return "\n".join(part for part in parts if part).lower()

然后根据技能名称、描述、分类、标签做匹配。

比如上下文里出现“战斗”“冲突”“爽点”“伏笔”“对话”“情感”“高潮”等词,就会匹配相关技能。

这一步其实也说明了上下文的价值。

上下文不只是给模型看的。

它也可以参与系统自己的决策:

选哪些技能
用什么模型
走哪个任务分支
是否需要审核
是否需要修复

如果上下文只是 prompt 字符串,就很难复用。

但如果上下文被结构化成对象、摘要、窗口、规则和步骤输出,它就能被系统多个模块共享。

当前实现还不够,所以要继续演进

重新扫代码时,我看到当前项目已经有了下一阶段设计:Agent 剧情规划与连续写作

这个设计里有几个关键词:

作品圣经
分层计划
上下文检索
连续性审核
人工审核提案
事实快照哈希

这说明上下文工程还要继续往前走。

当前版本能做到动态读取作品信息、角色、世界观、大纲、章节和规则。

但连续写作要更进一步。

因为连续写作不是“每次写一章”这么简单。

它还要知道:

哪些设定是作者批准过的
当前章节计划是否已批准
目标字数从哪里来
哪些人物状态不能擅自改变
哪些世界规则不能突破
哪些伏笔未回收
本次生成用了哪些事实来源
上下文有没有因为 token 预算被截断

所以后续设计里提出了“作品圣经”和“分层计划”。

作品圣经不是复制所有数据,而是保存作者确认过的全局规则和版本。

章节计划则明确本章目标、冲突、场景节拍、人物状态变化、伏笔和目标字数。

这样 Agent 就不是凭感觉写,而是基于批准过的生成契约写。

这也是上下文工程从“拼接事实”走向“可审核契约”的过程。

上下文工程的几个原则

结合这次项目,我目前会把 Agent 上下文工程拆成几个原则。

第一,上下文必须来自服务端事实源。

前端可以传用户意图,但不应该负责拼完整事实。

作品、角色、世界观、规则、大纲、章节历史这些都应该从后端统一读取。

第二,上下文必须按任务分层。

正文生成、章节规划、审核修复、记忆提取需要的上下文不同。

不要指望一个巨型 prompt 解决所有任务。

第三,上下文必须可裁剪。

长篇小说一定会越来越长。

如果不做窗口、摘要、优先级和截断记录,迟早会撞 token 限制。

第四,上下文必须可追溯。

Agent 生成了什么不够,还要知道它基于什么生成。

至少要记录关键来源、步骤输出和任务结果。

第五,上下文必须和权限绑定。

Agent 可以读什么、不能读什么,可以改什么、不能改什么,必须明确。

尤其是人物状态、世界规则、大纲方向这类高风险事实,不能让 Agent 静默改。

和第一篇的关系

第一篇里我提到一个观点:

用流程引擎式的确定性骨架,承载 LLM 的不确定性能力。

这一篇其实是在讲这个骨架里的第一块底板:上下文。

流程可以规定步骤怎么走。

工具可以规定能力怎么调用。

状态机可以规定任务怎么恢复。

但 Agent 每一步到底判断得对不对,首先取决于上下文是否可靠。

所以在我看来,Agent 落地的顺序不应该是:

先做一个自主 Agent
再想办法补上下文

而应该是:

先把上下文事实源、边界和审计做好
再逐步释放 Agent 的规划和决策空间

否则 Agent 越自主,风险越大。

最后

这一篇主要讲上下文工程。

它不是最炫的部分,但它决定了 Agent 后面能走多远。

在这个项目里,上下文已经从简单 prompt,逐步演进成:

动态 system prompt
章节上下文窗口
只读工具调用
章节规划输入
技能推荐依据
后续作品圣经和计划契约

这也是我现在对 AI Agent 实战越来越明确的判断:

Agent 不是先有“大脑”,再让它到处找信息;而是先把信息边界、工具边界和事实来源铺好,再让模型在这个边界里做判断。

下一篇我准备继续拆工具调用。

重点会讲:在真实业务系统里,哪些能力适合暴露成 Agent 工具,哪些能力不应该给 Agent 直接调用,以及工具权限应该怎么从业务风险倒推。

如果你也在做 AI Agent 应用,尤其是想把 Agent 从 Demo 推进到业务系统里,可以继续关注这个系列。