当前位置:首页 > 编程开发 > 正文

给 Agent 搭一层业务上下文:三层注入的实操与冲突裁决

约 7583 字 · 预计阅读 19 分钟
一句话总结:上一篇讲的「上下文层」不是买一个产品就能解决的,它落地是三件事——静态规则写进仓库、动态资源按需拉取、实时数据带权限查询。本文给出三层的具体写法、一个 40 行的冲突裁决器(实测能抓出「数据库指向主库还是从库」这类错配),以及五条踩过的坑。

上一篇拆 MIT 那份调研时留了个结论没展开:84% 的企业卡住,卡的不是模型,是模型不知道你们公司到底怎么运转。

这篇讲怎么补。不是讲该买什么产品,是讲三层上下文各写什么、放在哪、冲突了怎么办。

先说一个前提:这套东西小团队做一天能出雏形,大公司做要过跨部门协调。但代码结构是同一个。

前置条件

Python 3.9+(只用标准库,dataclasses 就够)
一个能调模型的客户端(Claude Code、Codex、OpenCode 都行)

不需要向量数据库,不需要 LangChain。三层上下文的第一版,纯文本加一个拼装函数就能跑。

三层的分工

先给结论,后面逐个展开。

层 载体 更新频率 谁维护 典型内容
项目层AGENTS.md,进 git随代码变开发团队技术栈、目录约定、禁改文件、测试命令
资源层MCP resource / 按需工具按需平台或运维表结构、指标口径、配置项、环境信息
业务层实时查询(SQL / API)实时数据团队当期数字、库存、订单状态、审批进度

三层不是可选项,缺一层就有一类任务做不了。但三层也不能一次全塞进 prompt,那会把上下文预算烧光。所以要分层、按需、有预算。

第一层:项目层,写死进仓库

这一层最简单,也最容易被忽略。

它解决的问题是:Agent 不知道你这个仓库的规矩。比如测试怎么跑、哪个目录别碰、日志用什么格式、提交前要过什么检查。

写法就是一串短句,不是文档。关键在于「可执行」而不是「全面」:

# AGENTS.md

## 命令
- 跑测试:`make test`(不要用 pytest 裸跑,会漏掉 fixture 初始化)
- 起本地服务:`make dev`,端口 8000
- 迁移:`alembic upgrade head`,改完必须先跑这个再跑测试

## 约定
- 所有 API 响应统一包 `{code, data, msg}`,错误码在 `app/errors.py`
- 时间一律 UTC 存储,展示层再转本地时区
- 日志用 structlog,不要 print

## 禁区
- `migrations/` 下的历史文件不要改
- `.env*` 不要读取、不要打印
- 不要自动执行 `git push`

三条经验。

只写 Agent 会犯错的地方。 你写「代码要整洁」等于没写,它本来就觉得自己写得整洁。写「测试必须用 make test 而不是 pytest」,这才是能拦住它的具体指令。

写完要验证。 找三个常见任务让 Agent 跑一遍,看它有没有照着做。没照做的条款,要么改成更明确的表述,要么删掉——一条不被执行的规则比没有规则更糟,因为它会稀释其他规则的权重。

这一层进 git,但不等于安全。 去年那份 AGENTS.md 投毒的案例说明,仓库里的文件能被第三方注入。所以工作目录的边界要划清,编码智能体隔离那篇里给了一套 CI 扫描的办法。

第二层:资源层,按需拉取

项目层是静态的,资源层是「需要时才读」的那一类。

典型内容:数据库表结构、指标的官方口径、环境变量说明、服务依赖关系。这些东西体积大、变化不频繁,全塞进 prompt 会挤掉真正的任务内容。

做法是做成 MCP 的一个 resource,Agent 需要时自己去读。用 FastMCP 写一个最小版:

from fastmcp import FastMCP

mcp = FastMCP("业务上下文")

@mcp.resource("schema://orders")
def orders_schema() -> str:
    """订单表结构。任何涉及订单金额的查询前先读这个。"""
    return """
orders 表
- id           bigint      主键
- user_id      bigint      用户 ID,关联 users.id
- amount_cents bigint      订单金额,单位:分(不是元)
- status       varchar     pending/paid/shipped/refunded
- created_at   timestamp   创建时间,UTC

注意:amount_cents 是分,展示时要除以 100。
      status = 'refunded' 的记录仍保留在原表,统计 GMV 时要排除。
"""

@mcp.resource("metrics://definition")
def metrics_definition() -> str:
    """核心指标的官方口径。口径不明时以这里为准。"""
    return """
月活(MAU):当月有至少一次登录的去重用户数,不含内部测试账号
GMV:当月 paid + shipped 状态的订单金额之和,不含退款
客单价:GMV / 当月支付订单数(不是除以用户数)
"""

这里有两个设计要点。

resource 的 docstring 就是「什么时候该读它」的说明。 上面那条「任何涉及订单金额的查询前先读这个」,比 resource 内容本身更重要——它在告诉模型触发条件。没有这句,模型不知道自己该去读。

口径定义要写成「排除了什么」。 光说「GMV 是订单金额之和」没意义,关键是「不含退款」。业务知识里有价值的部分几乎都是例外情况,不是通用定义。

MCP 服务本身的搭建细节,从零写一个 MCP 服务器那篇讲得比我细,这边只强调上下文层面的设计。

第三层:业务层,实时但要有边界

前两层解决「懂规矩」和「懂口径」,第三层解决「知道现在是多少」。

这一层最容易出事,因为它要连真数据。三条红线:

只给只读账号。 不要因为方便给它主库读写权限。这一条听起来像常识,但我在项目里见过为了「让它自动修数据」开写权限的,后来一次误判把状态字段刷了。

查询要走封装,不要让它直接拼 SQL。 给一组参数化的查询函数,而不是把数据库连接丢给它:

def get_mau(year: int, month: int) -> int:
    """按官方口径算月活。内部已排除内部账号。"""
    sql = """
        SELECT COUNT(DISTINCT user_id) FROM logins
        WHERE EXTRACT(YEAR FROM login_at) = %s
          AND EXTRACT(MONTH FROM login_at) = %s
          AND user_id NOT IN (SELECT id FROM users WHERE is_internal)
    """
    with readonly_conn() as c, c.Cursor() as cur:
        cur.execute(sql, (year, month))
        return cur.fetchone()[0]

返回值带上「什么时候查的」。 实时数据会过期,模型不知道这一点,可能拿昨天的数字回答今天的问题:

return {"value": n, "as_of": datetime.utcnow().isoformat() + "Z",
        "source": "logins 表", "definition": "metrics://definition#mau"}

带上 as_of 之后,模型在回答时至少有可能说「截至某时刻」。不带,它一定会把旧数字说成现在。

冲突裁决:三层说法不一致时听谁的

这是这套设计里最容易被跳过、但实际最要命的一步。

真实情况是:AGENTS.md 里写了「用只读从库」,MCP resource 里写了「主库」,某个查询函数的注释里又提到「生产库」。三处都对,但指向不同实例。模型读到三个说法,会挑一个最顺手的,而你不知道它挑了哪个。

解决办法是给每层定优先级,冲突时高优先级覆盖低优先级,并且把冲突本身暴露出来。

下面这个 40 行的裁决器我跑过,能同时完成去重和冲突检测:

from dataclasses import dataclass

@dataclass
class Fact:
    key: str
    value: str
    source: str        # project / resource / business
    priority: int      # 越小越优先

class ContextAssembler:
    def __init__(self):
        self.facts = []

    def add(self, key, value, source, priority):
        self.facts.append(Fact(key, value, source, priority))

    def conflicts(self):
        """同一个 key 有多个不同值时返回,用来暴露矛盾"""
        by_key = {}
        for f in self.facts:
            by_key.setdefault(f.key, []).append(f)
        return {k: v for k, v in by_key.items()
                if len({f.value for f in v}) > 1}

    def assemble(self, budget=600):
        """按优先级去重后拼成文本,超预算截断"""
        best = {}
        for f in sorted(self.facts, key=lambda x: x.priority):
            best.setdefault(f.key, f)
        lines = [f"- {f.key}: {f.value} [{f.source}]" for f in best.values()]
        return "\n".join(lines)[:budget]

跑一组刻意构造的冲突数据:

ca = ContextAssembler()
ca.add("数据库", "只读从库 analytics-ro", "project", 1)
ca.add("币种", "人民币,金额以分为单位", "project", 1)
ca.add("数据库", "主库 prod-main(读写)", "resource", 2)
ca.add("当前季度", "2026 Q3", "resource", 2)
ca.add("月活", "128,400", "business", 3)
ca.add("币种", "美元", "business", 3)

输出:

数据库:
  - 只读从库 analytics-ro  [project, 优先级 1]
  - 主库 prod-main(读写)  [resource, 优先级 2]
币种:
  - 人民币,金额以分为单位  [project, 优先级 1]
  - 美元  [business, 优先级 3]

裁决后:
- 数据库: 只读从库 analytics-ro [project]
- 币种: 人民币,金额以分为单位 [project]
- 当前季度: 2026 Q3 [resource]
- 月活: 128,400 [business]

6 条事实里抓出 2 处矛盾,裁决后剩 4 行。冲突列表要打进日志,不要静默处理——上面那两条冲突说明数据层和业务层的口径没有对齐,这是人的问题,得有人去改源头,代码替它决定只会把问题藏起来。

预算:上下文不是免费的

三层加起来很容易超过几万字。而每一轮对话都要重付一次,包括缓存命中的情况也不便宜。

实践里的三条约束:

项目层控制在 1000 字以内。 它每轮都在,最贵。写不进 1000 字的,说明你在写文档而不是写规则。

资源层按需,不预加载。 MCP resource 的好处就是模型自己去读。别在系统提示里塞「以下是所有表结构」。

业务层只带结论不带原始数据。 查 SQL 返回 5000 行明细,模型既读不完也用不上。在函数里就聚合好,返回一个数加一条口径说明。

图 1:左边三层各管什么、多久更新一次,右边是同一事实在三层说法不一致时的裁决结果

坑清单

按命中率排,第一条我踩过两次。

1. 三层的同一事实写了不同版本,谁都没发现。

最典型的是数据库地址、币种单位、指标口径。症状是 Agent 偶尔给出一个数量级差别很大的答案,你查半天以为是模型算错,其实是它读到了另一份定义。上线前先跑一次 conflicts(),把冲突清干净。

2. docstring 写成描述而不是触发条件。

写「订单表结构」,模型不知道什么时候该用;写「任何涉及订单金额的查询前先读这个」,它才会在正确时机去读。resource 和工具的说明都是给模型看的说明书,不是给人看的文档。

3. 把当前时间、用户名这类动态值写进静态层。

AGENTS.md 里写「今天是 2026-10-31」,第二天就错了,而且模型不会怀疑它。动态值一律放业务层,静态层只放不变的东西。

4. 返回数据不带时间戳和来源。

Agent 拿一个没有 as_of 的数字,会当成实时值来说。加上时间戳之后,至少有机会说「截至 X 时刻」。

5. 以为写了 AGENTS.md 就生效了。

工具不一定会读。Claude Code 会自动读项目根目录的,其他工具看配置。写完做一次实测:问它「这个项目怎么跑测试」,看它答的是不是文件里那句。答错了说明没生效,别以为是模型不听话。

收尾

三层讲完,回到 MIT 那份调研。它说 84% 的企业没拿到变革性价值,缺的是上下文层。我这篇给的是一份能落地的最小实现,加起来不到 150 行。

但我要说一句不那么乐观的话:这三层里最难的不是代码,是维护。

项目层会过期(命令改了没人更新),资源层会漂移(表结构变了口径没改),业务层会有权限事故。这些东西没有一次做完的版本,只有持续维护的流程。而这部分恰恰不属于任何人的 KPI,所以大多数团队做完第一版就放着烂了。

真要推,我的建议是先只做项目层,一个月后再做资源层,业务层放末尾。项目层成本最低、见效最快、也最能说服人——它能让 Agent 第一次在你的仓库里像个熟手,这个感受比任何 ROI 表格都有说服力。

下一篇讲验收标准:怎么判断一个 Agent 干得好不好,以及为什么没有验收标准就没法迭代。

参考资料

目录导航
  • 前置条件
  • 三层的分工
  • 第一层:项目层,写死进仓库
  • 第二层:资源层,按需拉取
  • 第三层:业务层,实时但要有边界
  • 冲突裁决:三层说法不一致时听谁的
  • 预算:上下文不是免费的
  • 坑清单
  • 收尾
    • 参考资料
  • 相关文章:

  • MIT 那份调研的真相:全员在用 AI,只有 16% 拿到变革性价值2026-10-31 08:44:36
  • Agent 的组织记忆:为什么「记住」比「更强」更难做2026-10-31 08:44:36
  • 给 Agent 加预算护栏:四道闸门、一个检查点和分级降级2026-10-31 08:44:36
  • prompt caching 省钱实战:缓存价差 10 倍,但九成人的 prompt 结构吃不到2026-10-31 08:44:36
  • 腾讯混元 Hy4 正式版预告:模型之外,Buddy One 才是这次真正的新东西2026-10-31 08:44:36
  • Agent 上线之后:工具调用日志、token 账和失败归因怎么做2026-10-31 08:44:36
  • 写一个最小的 Agent 循环:80 行代码,和三个必须处理的边界2026-10-31 08:44:36
  • 从零写一个 MCP 服务器:让你的脚本变成 AI 能直接调的工具2026-10-31 08:44:36
  • 发表评论

    ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。