![]() 玄同 765 AI Engineer | ▍深耕领域 AI Agent 架构设计 Agent Harness 工程 Context Engineering 多智能体协同 & RAG |
| 「 让 AI 交互更智能,让技术落地更高效 」 | 欢迎技术探讨 / 项目合作,关注解锁大模型与智能交互的无限可能! | |
【Agent Harness 架构手册 #04】企业级 AI Agent 零信任权限审计链(Permission Evidence)与 AST 行级安全注入:防止长对话中途“权限收回后越权泄密”
📚 《Agent Harness 架构手册:构建高可靠 AI 智能体运行时》五篇连载导航
- #01 配额与自愈:LangGraph 动态工具配额守卫实战:根治无限探索与 return_direct 报错不自愈
- #02 可观测交互:AI Agent 实时动态流(Action Feed)与阶段性结论合成引擎实战
- #03 上下文投影:多轮对话上下文引擎实战:L1~L4 四级预算投影与无损回放
- #04 零信任安全:企业级 Agent 零信任权限审计与 AST 行级安全注入:防止长对话中途撤权泄密
- #05 自进化与重连:AI Agent 自进化经验蒸馏飞轮与 SSE 游标断点重连实战
在前几篇连载中,我们先后搭建了 动态工具配额守卫(#01)、声明式 Action Feed 实时动态流(#02) 与 L1~L4 四级无损历史投影引擎(#03)。当这套 Agent 系统真正进入金融、电商、政企等生产环境时,信息安全团队(SecOps)往往会抛出三个能直接一票否决上线的灵魂拷问:
- 长对话中途“权限撤回”引发的上下文残留泄密:
假设分析师小张在第 1 轮对话时拥有华东区 + 华南区权限,Agent 调用工具查出了华东区的核心营收明细并留在了多轮对话历史里;到了第 3 轮,管理员在后台紧急收回了小张的华东区权限。此时小张不调任何新工具,直接问一句:“把你第 1 轮查出来的华东区前 5 名大客户再给我列一下”——如果只拦截工具调用而不治理历史消息,大模型会直接看着第 1 轮的历史记录把已撤权的机密数据背出来! - 工具执行一半时权限发生变更(TOCTOU 竞态漏洞):
某条复杂 SQL 查询耗时 6 秒,在第 1 秒发起查询时用户还有权限,第 3 秒管理员撤回了权限,第 6 秒查询结果返回——怎么保证这条“执行中途失权”的数据绝对不会进入ToolMessage和前端事件流? - 靠 Prompt 叮嘱模型“记得加
WHERE region = '华南'”形同虚设:
把行级权限(Row-Level Security)写在 System Prompt 里让大模型自己拼 SQL,一旦遇到UNION ALL、公用表表达式(WITH cte AS ...)或子查询,模型极易漏加过滤条件甚至被 Prompt 注入绕过!
本文将带你从零实现企业级 「三重零信任权限审计链中间件(Permission Evidence Middleware)」 + 「基于权限代次(Grant Epoch)的旧上下文自动隔离」 + 「基于 sqlglot AST 语法树的确定性行级过滤强制注入」,打造滴水不漏的 Fail-Closed 安全防线!
文章目录
- 【Agent Harness 架构手册 #04】企业级 AI Agent 零信任权限审计链(Permission Evidence)与 AST 行级安全注入:防止长对话中途“权限收回后越权泄密”
一、为什么“只在工具入口查一次权限”挡不住大模型泄密?(原理拆解篇)
1.1 从“机密档案室的门禁卡注销”与“手里已复印出的旧文件”说起
为什么传统 Web 后端那套“在 API 入口加一个 @require_permission 装饰器”的做法,搬到多轮对话 AI Agent 里会彻底失效?
想象一下机密档案室的管理制度:
- 传统 Web 接口(单次无状态请求):你刷门禁卡进门,拿一份文件出来。只要把你的门禁卡注销,你下一次就进不去门了。
- 多轮对话 AI Agent(带记忆的有状态推理):
- 手里还捏着上一轮复印出来的旧档案(历史消息残留):小张虽然门禁卡被改成了“仅限华南区”,但他和小秘书(LLM)的办公桌上(
messages历史上下文里),还摊着 5 分钟前刚印出来的《华东区绝密名单》。只要小张开口问小秘书,小秘书根本不用再去刷门禁卡调工具,低头看一眼桌面就能把名单念出来! - 进门时卡有效,出门时卡已注销(工具执行期竞态):小秘书刷卡进档案室找资料花了 10 秒钟,第 5 秒时保安在后台注销了这张卡。如果不在出门(结果写入
ToolMessage并推送前端)前再验一次卡,绝密资料照样会被带出房间!
- 手里还捏着上一轮复印出来的旧档案(历史消息残留):小张虽然门禁卡被改成了“仅限华南区”,但他和小秘书(LLM)的办公桌上(
因此,生产级 Agent 的权限控制必须升级为 「全生命周期权限纪元(Grant Epoch)+ 三重实时核验 + AST 语法树硬改写」 架构:
1.2 四种 Agent 数据安全与权限控制方案全景对比
在企业级 Data Agent 架构选型中,我们将四种常见的权限控制层级对比如下:
| 方案名称 | 行级过滤实现方式 | 能否防御长对话中途撤权泄密? | 能否防御工具执行期并发撤权(TOCTOU)? | 对复杂 SQL(CTE / UNION)安全性 | 推荐指数 |
|---|---|---|---|---|---|
| 方案 A:Prompt 软提示词约束 | 让 LLM 自己往生成的 SQL 里拼 WHERE | 否 | 否 | 极差(100% 可被诱导绕过) | ⭐ |
方案 B:正则表达式拼接 WHERE | 用字符串替换在 SQL 末尾追加 AND ... | 否 | 否 | 差(遇到 GROUP BY / LIMIT / UNION 直接语法报错或逻辑错乱) | ⭐⭐ |
| 方案 C:仅在工具函数入口做 RBAC 校验 | 工具执行前查表级权限 | 否(历史轮次里的旧结果仍对模型可见) | 否(长耗时查询返回时不复核) | 中 | ⭐⭐⭐ |
| 方案 D:三重证据链 + 权限代次隔离 + AST 注入(本文方案) | sqlglot AST 将目标表替换为受限安全子查询 | 100% 防御(版本变更自动遮罩旧代次结果并注入证据清单) | 100% 防御(工具返回后、入模前二次复核 verify_grant_still_valid) | 极高(语法树级等价重写,天然免疫注入) | ⭐⭐⭐⭐⭐ |
💡 核心安全铁律:
- 永远只从受保护的隐藏运行时上下文(
runtime.context)读取用户身份与权限,严禁从大模型生成的工具参数或对话消息中推导权限! - 任何未经“发布前二次核验”的工具输出,严禁进入
ToolMessage或通过on_tool_end暴露给前端!
二、基础篇:3 分钟实现“三重实时权限核验”与可信证据戳(RealtimeAccessEnforcerMiddleware)
我们先来构建第一道核心防线:怎么保证在一次 agent.run() 的每一个关键节点(调模型前、调工具前、工具返回后),一旦后台权限发生任何风吹草动,立即阻断或标记证据状态?
2.1 为什么要单独派发 permission_tool_result 而不是直接用 on_tool_end?
这是一个极其关键的工程细节!
在 LangChain / LangGraph 中,BaseTool 自身的 on_tool_end 回调是在工具函数 func() 刚刚 return 的那一瞬间触发的——此时外层的 AgentMiddleware.awrap_tool_call 还没来得及执行“结果发布前二次权限核验(Post-Execution Check)”!
如果在那短短几秒内管理员刚刚撤回了用户的权限,直接监听 on_tool_end 的宿主服务就会把未通过二次核验的数据泄露给前端!
因此,我们在中间件里遵循如下严格时序:
await run.verify_grant_still_valid()(第 1 次核验:工具执行前);result = await handler(request)(执行底层工具);await run.verify_grant_still_valid()(第 2 次核验:工具返回后、结果发布前!若此时发现权限版本号permission_grant_version已变更,直接抛出异常中断,绝不让结果进入ToolMessage);- 为结果打上不可伪造的
[可信证据标识](Permission Evidence Record),并显式派发经过二次核验的自定义事件permission_tool_result!
2.2 完整可复用代码:ZeroTrustAccessGuard 与三重核验中间件
from __future__ import annotations
import hashlib, json, time
from dataclasses import asdict, dataclass, field
from typing import Any, Awaitable, Callable
from langchain_core.messages import SystemMessage, ToolMessage
# 需要纳入零信任权限审计链监管的敏感数据工具集合
PROTECTED_DATA_TOOLS = frozenset({
"execute_sql_query",
"search_data_catalog",
"inspect_table_schema",
"run_python_sandbox",
"read_sandbox_output",
})
class AccessRevokedException(RuntimeError):
"""当检测到用户权限版本号(permission_grant_version)在运行期发生变更时抛出。"""
@dataclass(frozen=True)
class AccessAuditEntry:
"""单次受保护工具调用的可信权限证据戳。"""
op_name: str
tool_call_id: str
permission_grant_version: str
output_sha256: str
verified_at: float
status: str = "valid"
@dataclass
class ZeroTrustAccessGuard:
"""绑定在单次运行隐藏上下文(runtime.context)中的权限核验器。"""
user_id: str
tenant_space_id: str
initial_grant_version: str
fetch_current_grant_version: Callable[[], Awaitable[str]]
prior_valid_refs: list[str] = field(default_factory=list)
revoked_refs: list[str] = field(default_factory=list)
audit_log_entries: list[AccessAuditEntry] = field(default_factory=list)
async def verify_grant_still_valid(self) -> None:
"""实时向权限中心/缓存核对当前最新 permission_grant_version,若不一致立即 Fail-Closed 中断。"""
latest_rev = await self.fetch_current_grant_version()
if latest_rev != self.initial_grant_version:
raise AccessRevokedException(
f"用户 {self.user_id} 权限版本已在运行期变更 ({self.initial_grant_version} -> {latest_rev}),已安全中止当前操作。"
)
def append_audit_entry(self, op_name: str, tool_call_id: str, text: str, status: str = "success") -> AccessAuditEntry | None:
if status == "error" or text.lstrip().lower().startswith(("error:", "sql error:")):
return None
digest = hashlib.sha256(text.encode("utf-8")).hexdigest()[:16]
rec = AccessAuditEntry(
op_name=op_name,
tool_call_id=tool_call_id,
permission_grant_version=self.initial_grant_version,
output_sha256=digest,
verified_at= round(time.time(), 3),
)
self.audit_log_entries.append(rec)
return rec
def build_evidence_prompt(self) -> str:
"""生成注入到 SystemMessage 尾部的可信权限证据清单提示。"""
if not self.prior_valid_refs and not self.revoked_refs:
return ""
lines = ["\n## 零信任权限审计链安全约束(Fail-Closed)"]
lines.append(f"- 当前生效权限版本纪元(Epoch):`{self.initial_grant_version}`")
if self.revoked_refs:
lines.append(
f"- 已失效/被撤权的历史调用引用:`{', '.join(self.revoked_refs)}`"
"(严禁引用、复述或推断上述已失效调用的任何数据内容,需重新调用工具获取当前授权范围内的数据!)"
)
return "\n".join(lines)
class RealtimeAccessEnforcerMiddleware:
"""在模型调用前、受保护工具执行前后分别执行 Fail-Closed 权限核验的中间件。"""
@staticmethod
def _get_run(request: Any) -> ZeroTrustAccessGuard | None:
# 严禁从模型消息或工具参数中读取权限对象,仅从宿主隐藏 runtime.context 读取
ctx = getattr(getattr(request, "runtime", None), "context", None)
return ctx.get("access_audit_trail") if isinstance(ctx, dict) else None
async def awrap_model_call(self, request: Any, handler: Callable[[Any], Awaitable[Any]]) -> Any:
run = self._get_run(request)
if run is not None:
# 核验点 1:每次请求大模型前校验实时权限版本,并注入新旧证据可信清单
await run.verify_grant_still_valid()
prompt_addon = run.build_evidence_prompt()
if prompt_addon:
sys_msg = request.system_message
old_text = sys_msg.content if sys_msg and isinstance(sys_msg.content, str) else ""
request = request.override(system_message=SystemMessage(content=old_text + prompt_addon))
return await handler(request)
async def awrap_tool_call(self, request: Any, handler: Callable[[Any], Awaitable[Any]]) -> Any:
run = self._get_run(request)
op_name = request.tool_call["name"]
if run is None:
return await handler(request)
# 核验点 2:工具真实执行前校验权限
await run.verify_grant_still_valid()
result = await handler(request)
# 核验点 3:工具执行返回后、写入 ToolMessage 前二次复核!杜绝执行期并发撤权泄密
await run.verify_grant_still_valid()
if op_name not in PROTECTED_DATA_TOOLS or not isinstance(result, ToolMessage):
return result
text = result.content if isinstance(result.content, str) else json.dumps(result.content, ensure_ascii=False)
rec = run.append_audit_entry(op_name, request.tool_call["id"], text, getattr(result, "status", "success"))
if rec is not None:
artifact = dict(result.artifact) if isinstance(result.artifact, dict) else {}
artifact["access_audit_trail"] = asdict(rec)
suffix = "\n[可信证据标识] " + json.dumps(asdict(rec), ensure_ascii=False)
result = result.model_copy(update={"content": text + suffix, "artifact": artifact})
return result
三、进阶篇:权限代次(Grant Epoch)隔离与历史上下文自动脱敏实战
现在我们解决文章开头提出的第 1 个尖锐问题:当用户在第 1~2 轮时的权限版本是 rev_v1,到了第 3 轮管理员改了他的权限,使他的实时权限版本变成了 rev_v2(开启了新的权限代次 Grant Epoch)。此时回放第 1~2 轮历史消息时,怎么防止旧代次的机密数据流入第 3 轮的大模型?
3.1 全链路架构拆解:跨权限代次(Cross-Epoch)的历史证据清洗与压缩继承
我们在回放历史消息(结合上一篇 #03 的 Context Engine v2)时,对每一条历史 ToolMessage 检查它身上的 [可信证据标识]:
- 同代次放行(Same Epoch):如果历史工具结果的
permission_grant_version == current_grant_version,说明权限未发生变更,正常按L1/L2规则回放; - 跨代次自动遮罩(Cross-Epoch Redaction):如果历史工具结果的
permission_grant_version != current_grant_version(或者缺失可信证据标识),说明该结果产出于已过期的旧权限代次!系统立即将其正文替换为:
[历史结果已因权限版本变更而隔离|tool=execute_sql_query|old_rev=rev_v1 -> current_rev=rev_v2|请重新调用工具获取当前权限范围的数据],同时将该tool_call_id记入revoked_refs! - 新代次内恢复压缩(Epoch-Scoped Compaction):在做上下文检查点摘要压缩时,摘要生成器的采样起点自动锚定在当前最新权限代次的第一条消息,绝不把旧代次的敏感数字混入新代次的文本摘要中!
3.2 完整可复用代码:跨权限代次(Grant Epoch)历史清洗器
EVIDENCE_MARKER = "\n[可信证据标识] "
def parse_message_evidence(content: str) -> tuple[str, dict[str, Any] | None]:
"""从 ToolMessage 内容尾部解析并分离 (纯正文, 可信证据字典)。"""
text = str(content or "")
if EVIDENCE_MARKER not in text:
return text, None
body, raw_json = text.rsplit(EVIDENCE_MARKER, 1)
try:
ev = json.loads(raw_json.strip())
return body, ev if isinstance(ev, dict) else None
except Exception:
return body, None
def sanitize_history_by_permission_epoch(
messages: list[dict[str, Any]],
*,
current_grant_version: str,
) -> tuple[list[dict[str, Any]], list[str], list[str]]:
"""
按当前实时权限版本号(current_grant_version)清洗历史消息链:
- 同代次(grant_version 一致)的受保护工具结果予以保留;
- 跨代次(旧 grant_version 或无证据戳)的受保护工具结果强制遮罩,防止撤权后上下文残留泄密。
返回: (清洗后的消息列表, 有效引用列表 prior_valid_refs, 已撤权引用列表 revoked_refs)
"""
sanitized: list[dict[str, Any]] = []
valid_refs: list[str] = []
revoked_refs: list[str] = []
tainted_turn = False
for msg in messages:
role = msg.get("role")
if role == "user":
tainted_turn = False
sanitized.append(dict(msg))
continue
if role == "tool":
t_name = str(msg.get("name") or "")
call_id = str(msg.get("tool_call_id") or "")
body, ev = parse_message_evidence(str(msg.get("content") or ""))
if t_name in PROTECTED_DATA_TOOLS:
msg_rev = ev.get("permission_grant_version") if ev else None
if msg_rev == current_grant_version:
valid_refs.append(call_id)
sanitized.append({**msg, "content": body})
else:
# 检测到跨权限代次!立即执行 Fail-Closed 遮罩
revoked_refs.append(call_id)
tainted_turn = True
masked_text = (
f"[安全隔离:该历史工具结果产出于旧权限版本 ({msg_rev or 'unverified'}),"
f"当前生效权限版本为 ({current_grant_version})。旧数据已隐藏,请重新调用 {t_name} 查询最新授权数据。]"
)
sanitized.append({**msg, "content": masked_text})
else:
sanitized.append({**msg, "content": body})
continue
if role == "assistant" and tainted_turn and not msg.get("tool_calls"):
# 若该轮包含已被撤权隔离的敏感查询,对当时生成的最终文本回答同步加上重查警示
sanitized.append({
**msg,
"content": "[注:本轮历史回答基于变更前的旧权限数据生成,数值已隐藏,后续提问请重新调用查询工具核实。]",
})
continue
sanitized.append(dict(msg))
return sanitized, valid_refs, revoked_refs
四、高阶技巧:基于 sqlglot AST 语法树的确定性行级安全(RLS)强制注入
解决了上下文层的权限代次隔离后,我们再来加固最底层的 SQL 执行引擎:当模型生成了一条复杂的 SQL 查询(可能包含多表 JOIN、子查询、CTE (WITH ...) 甚至 UNION)时,怎么在不依赖大模型自觉性的前提下,100% 强制把用户的行级权限过滤条件(如 region IN ('华南', '西南') AND dept_id = 102)注入进去?
4.1 为什么不能直接往外层 WHERE 拼字符串,而必须做“表节点子查询包裹”?
假设用户的行级权限规则是:访问订单视图 dw_orders 时,必须满足 region = '华南'。
如果大模型写了一条带 LEFT JOIN 或 WITH 的 SQL:
WITH top_regions AS (
SELECT region, SUM(amount) AS total_amt FROM dw_orders GROUP BY region
)
SELECT * FROM top_regions
如果你只是呆板地在整条 SQL 的最外层加一个 WHERE region = '华南',在有些聚合或排名(如 RANK() OVER (ORDER BY amount DESC))场景下,模型依然能通过排名序号推算出华东区的业绩排第几!
更安全、更彻底的做法是:利用 sqlglot 将 SQL 解析为抽象语法树(AST),遍历所有 exp.Table 节点(排除 CTE 别名),直接把目标表 dw_orders 原地替换为一个内联受限子查询 (SELECT * FROM dw_orders WHERE region = '华南') AS dw_orders!
4.2 完整可复用代码:基于 sqlglot 的 AST 行级权限重写与方言转译器
import sqlglot
from sqlglot import exp
@dataclass(frozen=True)
class RowFilterRule:
"""结构化行级权限过滤规则(支持 =, !=, IN, NOT IN 等安全算子)。"""
filter_col_name: str
operator: str # "eq" | "in" | "not_in"
values: tuple[str | int | float, ...]
def to_sql_condition(self) -> exp.Expression:
col_expr = exp.filter_col_name(self.filter_col_name)
literals = [exp.Literal.string(str(v)) if isinstance(v, str) else exp.Literal.number(v) for v in self.values]
if self.operator == "eq":
return exp.EQ(this=col_expr, expression=literals[0])
if self.operator == "in":
return exp.In(this=col_expr, expressions=literals)
if self.operator == "not_in":
return exp.Not(this=exp.In(this=col_expr, expressions=literals))
raise ValueError(f"Unsupported row filter operator: {self.operator}")
def apply_ast_row_level_security(
sql: str,
table_policies: dict[str, list[RowFilterRule]],
*,
read_dialect: str = "postgres",
write_dialect: str = "postgres",
) -> str:
"""
使用 sqlglot 在 AST 语法树层将受保护表原地替换为带 WHERE 过滤的内联子查询,
确保任何 CTE、JOIN、窗口函数都只能在授权行子集上运算。
"""
parsed_list = sqlglot.parse(sql, read=read_dialect)
if len(parsed_list) != 1 or parsed_list[0] is None:
raise ValueError("安全拦截:仅允许执行单条 SELECT 查询语句")
root = parsed_list[0]
if not isinstance(root, (exp.Select, exp.Union)):
raise ValueError("安全拦截:禁止执行非查询类(DDL/DML)SQL 语句")
# 1. 收集所有 CTE 别名,防止把 WITH 临时表误当成物理表包裹
cte_names = {cte.alias_or_name.lower() for cte in root.find_all(exp.CTE) if cte.alias_or_name}
# 2. 收集语法树中的所有真实 Table 节点并原地改写
for table_node in list(root.find_all(exp.Table)):
t_name = (table_node.name or "").lower()
if not t_name or t_name in cte_names or t_name not in table_policies:
continue
rules = table_policies[t_name]
if not rules:
continue
# 组合多条过滤规则为 AND 表达式
conditions = [r.to_sql_condition() for r in rules]
where_expr = conditions[0]
for cond in conditions[1:]:
where_expr = exp.And(this=where_expr, expression=cond)
# 保留原表的别名(若原表写了 FROM dw_orders AS o,则子查询别名仍为 o;否则别名为原表名)
effective_alias = table_node.alias or table_node.name
clean_table = exp.Table(this=exp.Identifier(this=table_node.name), db=table_node.args.get("db"))
subquery_select = exp.select("*").from_(clean_table).where(where_expr)
wrapped_subquery = exp.Subquery(this=subquery_select, alias=exp.TableAlias(this=exp.Identifier(this=effective_alias)))
table_node.replace(wrapped_subquery)
return root.sql(dialect=write_dialect)
五、实战避坑指南:高频报错与一键解决速查表
在将零信任权限审计链与 AST 行级安全注入落地到生产环境时,如果遇到以下典型问题,可对照下表快速排查:
| 序号 | 遇到的报错或现象 | 背后的真正原因 | 一句话解决办法 |
|---|---|---|---|
| 01 | AST 改写后报 ambiguous column name 或找不到原表名引用(如 SELECT dw_orders.gmv FROM ...) | 将 dw_orders 替换为子查询时忘了给子查询加上与原表名(或原别名)完全一致的 AS dw_orders 别名 | 在构造 exp.Subquery 时必须设置 alias = table_node.alias or table_node.name,保持外部列引用 100% 兼容 |
| 02 | SQL 中定义了 WITH sales AS (...),结果 AST 改写器在 sales 引用处又套了一层过滤导致循环报错 | 未区分物理表与 WITH (CTE) 定义的临时视图名 | 在遍历 root.find_all(exp.Table) 前,先提取 {cte.alias_or_name.lower() for cte in root.find_all(exp.CTE)} 并跳过 |
| 03 | 某张受保护视图配置了行级权限,但用户当前被分配的 values 列表为空(没有任何可访问区域),生成的 IN () 报 SQL 语法错误 | 空列表直接拼接成 col IN () 在标准 SQL 中是非法语法 | 当检测到某张表的受限规则 values 为空集时,直接在 AST 改写前 Fail-Closed 抛出“无任何行级访问权限”或生成 WHERE 1 = 0 |
| 04 | 权限变更后,ToolMessage 虽然被遮罩了,但大模型还是从上一轮 Assistant 的文字回答里读到了旧数据 | 跨代次清洗时只遮罩了 role == "tool",漏掉了同一轮里复述了该数据的 role == "assistant" 最终总结 | 在 sanitize_history_by_permission_epoch 中标记 tainted_turn = True,将该轮对应的 Assistant 结论同步替换为重查提示 |
| 05 | 业务库是 MySQL / ClickHouse,模型写出了 Postgres 方言的 DATE_TRUNC 导致查询报错 | AST 改写与方言转译未打通 | 在 apply_ast_row_level_security 的最后一步 root.sql(dialect=write_dialect) 中直接传入目标数据库方言,一步完成安全注入与跨方言转译 |
结语
回顾本篇《Agent Harness 架构手册 #04》,我们构建了一套真正经得起红蓝对抗检验的企业级 AI Agent 零信任数据安全防线:
- 三重实时核验(
RealtimeAccessEnforcerMiddleware):在模型调用前、工具执行前、结果发布前三次校验permission_grant_version,彻底消灭工具执行中途撤权的 TOCTOU 竞态漏洞; - 权限代次隔离(Grant Epoch):为每条敏感输出加盖不可伪造的权限证据戳,一旦权限版本变更,自动隔离旧代次历史数据,防止大模型“凭记忆越权背诵”;
sqlglotAST 语法树级行级过滤注入:把受保护表在语法树层原地替换为受限子查询,无论 SQL 多复杂都绝不漏行、绝不越权。
👉 下一篇收官预告(#05):当我们的 Agent 拥有了强大的安全底座与上下文引擎后,怎么让它具备 “从用户的每一次点赞与纠错中自动提炼沉淀新技能/新视图提案的自进化能力(Knowledge Flywheel)”?又怎么实现 “哪怕用户刷新浏览器页面也能无缝续接流式输出的 SSE 断点续传与中途控向(Replayable Stream & Steer)”?收官篇我们一探究竟!





104



被折叠的 条评论
为什么被折叠?



