commerce-agents.py
§ · Nested detail page

Grounding、Fencing 与 Memory 深度解析

这一页关注输入可信度:外部文本如何被围栏包住,提示块如何带上动态上下文,记忆事实又怎样在写入前被过滤和分层挑选。

§ 00 · IMPORT CHAIN

导入链与契约

公共安全层先把外部文本降级为 data,再决定是否强制调用 grounding 工具;入口散布在 sources/commerce-agents/commerce-common/commerce_common/fencing.py:20、sources/commerce-agents/commerce-common/commerce_common/fencing.py:83、sources/commerce-agents/commerce-common/commerce_common/grounding.py:55、sources/commerce-agents/commerce-common/commerce_common/prompt_assembly.py:33,这让 prompt 不必假设检索内容天然可信。

src/sources/commerce-agents/commerce-common/commerce_common/fencing.py · L83-120Python
  83 @dataclass(frozen=True)
  84 class Fence:
  85     """The tag that wraps third-party content and the notice the static prompt carries
  86     about it."""
  87 
  88     label: str
  89     notice: str
  90 
  91     @property
  92     def open(self) -> str:
  93         return f"<{self.label}>"
  94 
  95     @property
  96     def close(self) -> str:
  97         return f"</{self.label}>"
  98 
  99     def sanitize_text(self, text: str, max_chars: int | None = None) -> str:
 100         """``max_chars`` bounds the result including the truncation suffix, so a schema
 101         limit can be passed as is."""
 102         text = unicodedata.normalize("NFKC", text)
 103         text = _INVISIBLE.sub("", text)
 104         text = _CONTROL.sub(" ", text)
 105         # Markers and tokens are removed to a fixpoint, so one nested inside another
 106         # (``</label</label>>``) does not reassemble after the inner one goes.
 107         marker = _marker_pattern(self.label)
 108         while True:
 109             stripped = _SPECIAL_TOKEN.sub("[removed]", marker.sub("[removed]", text))
 110             if stripped == text:
 111                 break
 112             text = stripped
 113         text = _TURN_INDICATOR.sub(r"\1\2 -", text)
 114         if max_chars is not None and len(text) > max_chars:
 115             suffix = " ...[truncated]"
 116             if max_chars > len(suffix):
 117                 text = text[: max_chars - len(suffix)] + suffix
 118             else:
 119                 text = text[:max_chars]
 120         return text
§ 01 · CORE IMPL

核心实现,逐段拆

▸Prompt assembly 把 cache-control 与 dynamic context 分开

build_request_messages 先构建系统块,再决定 eager input、tool cache-control 和历史消息如何组合;sources/commerce-agents/commerce-common/commerce_common/prompt_assembly.py:25、sources/commerce-agents/commerce-common/commerce_common/prompt_assembly.py:43、sources/commerce-agents/commerce-common/commerce_common/prompt_assembly.py:52、sources/commerce-agents/commerce-common/commerce_common/prompt_assembly.py:60、sources/commerce-agents/shopping-agent/core/shopping_agent/prompt.py:197 显示它把稳定规则和每轮上下文分开处理。

src/sources/commerce-agents/commerce-common/commerce_common/prompt_assembly.py · L60-119Python
  60 def build_request_messages(
  61     messages: list[dict[str, Any]],
  62     *,
  63     rolling_breakpoint: bool = True,
  64 ) -> list[dict[str, Any]]:
  65     """The outgoing request's messages: a request-shaped copy of ``messages`` with the
  66     rolling cache breakpoint on the newest persisted content block.
  67 
  68     Within a turn the system blocks are constant, so the marker makes each round's prior
  69     rounds — the long tool results especially — a cache read instead of a reprocess. Two
  70     cases skip it. A bare first call (one message) would write an entry a one-shot
  71     session never reads, and the next call marks a later block whose span carries the
  72     first message anyway. And the caller passes ``rolling_breakpoint=False`` on rounds
  73     whose ``tool_choice`` is not ``auto``, because ``tool_choice`` keys the messages
  74     span: an entry written under a forced round is unreadable by the auto rounds that
  75     follow.
  76 
  77     A turn that closed on a presentation round leaves a tool_result message followed by
  78     the next user message; the two go out as one user message, tool_result blocks first.
  79 
  80     Applied per call, to the outgoing request only: the returned list shallow-copies the
  81     touched message and blocks, strips the marker any earlier call placed, and never
  82     mutates the host's persisted history. String content is lifted into a one-block list
  83     because ``cache_control`` lives on content blocks.
  84     """
  85     if not messages:
  86         return []
  87 
  88     def without_marker(message: dict[str, Any]) -> dict[str, Any]:
  89         content = message.get("content")
  90         if not isinstance(content, list) or not any(
  91             isinstance(b, dict) and "cache_control" in b for b in content
  92         ):
  93             return message
  94         return message | {
  95             "content": [
  96                 {k: v for k, v in b.items() if k != "cache_control"} if isinstance(b, dict) else b
  97                 for b in content
  98             ]
  99         }
 100 
 101     def blocks(raw: Any) -> list[Any]:
 102         return [{"type": "text", "text": raw}] if isinstance(raw, str) else list(raw or [])
 103 
 104     request: list[dict[str, Any]] = []
 105     for message in messages:
 106         message = without_marker(message)
 107         if request and message.get("role") == "user" and request[-1].get("role") == "user":
 108             request[-1] = request[-1] | {
 109                 "content": blocks(request[-1].get("content")) + blocks(message.get("content"))
 110             }
 111         else:
 112             request.append(message)
 113     if not rolling_breakpoint or len(request) < 2:
 114         return request
 115     content = blocks(request[-1].get("content"))
 116     if content and isinstance(content[-1], dict):
 117         content[-1] = {**content[-1], "cache_control": {"type": "ephemeral"}}
 118         request[-1] = request[-1] | {"content": content}
 119     return request

▸MemoryWriteFilter 拦截敏感与短期事实

记忆层并非把所有对话都保存;sources/commerce-agents/commerce-common/commerce_common/memory.py:78、sources/commerce-agents/commerce-common/commerce_common/memory.py:101、sources/commerce-agents/commerce-common/commerce_common/memory.py:130、sources/commerce-agents/commerce-common/commerce_common/memory.py:150、sources/commerce-agents/commerce-common/commerce_common/memory.py:216 先过滤禁止模式、会话性内容和无效 fact,再挑选 tier-one fact 注入后续请求。

src/sources/commerce-agents/commerce-common/commerce_common/memory.py · L78-118Python
  78 class MemoryStore(Protocol):
  79     """Implement against your own storage. ``subject_id`` is the user or merchant the
  80     facts belong to."""
  81 
  82     async def get_facts(self, subject_id: str) -> list[MemoryFact]: ...
  83 
  84     async def upsert_facts(self, subject_id: str, facts: list[MemoryFact]) -> None: ...
  85 
  86     async def search_facts(self, subject_id: str, query: str) -> list[MemoryFact]: ...
  87 
  88     async def delete_fact(self, subject_id: str, key: str) -> bool:
  89         """Remove one fact by key; True when something was deleted."""
  90         ...
  91 
  92     async def clear(self, subject_id: str) -> None:
  93         """Purge every fact for the subject and advance its purge generation."""
  94         ...
  95 
  96     async def purge_generation(self, subject_id: str) -> int:
  97         """How many times the subject has been purged; 0 for a subject never purged."""
  98         ...
  99 
 100 
 101 MEMORY_STORE_METHODS: tuple[str, ...] = tuple(
 102     name
 103     for name, member in vars(MemoryStore).items()
 104     if callable(member) and not name.startswith("_")
 105 )
 106 
 107 
 108 def check_memory_store(store: MemoryStore) -> MemoryStore:
 109     """Raise ``TypeError`` naming the first :class:`MemoryStore` method ``store`` lacks.
 110     Runs where a store enters a deployment, so a partial store fails at startup instead
 111     of inside the post-turn extraction pass, whose failures do not stop the turn."""
 112     for name in MEMORY_STORE_METHODS:
 113         if not callable(getattr(store, name, None)):
 114             raise TypeError(
 115                 f"{type(store).__name__} does not implement MemoryStore.{name}; the store "
 116                 f"contract is {', '.join(MEMORY_STORE_METHODS)}"
 117             )
 118     return store
§ 02 · KEY FLOW

关键流程

external contentstrip controls + fence
→
grounding ruleterm and cue match
→
prompt blockssystem + dynamic context
→
memory factsfilter + select

这条路径的强点是每一步都可以单测:sources/commerce-agents/commerce-common/tests/test_fencing.py:1、sources/commerce-agents/commerce-common/tests/test_grounding.py:1、sources/commerce-agents/commerce-common/tests/test_prompt_assembly.py:1、sources/commerce-agents/commerce-common/tests/test_memory_facts.py:1、sources/commerce-agents/commerce-common/tests/test_memory_runtime.py:1 分别检查文本清洗、工具强制、prompt 结构和 memory runtime。

§ 03 · EDGE CASES

边界 / 失败模式

  • sources/commerce-agents/commerce-common/commerce_common/fencing.py:42 会清理 bidi/control 字符,避免不可见文本改变 prompt 语义。
  • sources/commerce-agents/commerce-common/commerce_common/grounding.py:72 只返回第一个强制工具,因此多规则优先级要在配置里明确。
  • sources/commerce-agents/commerce-common/commerce_common/memory.py:183 的匹配选择偏轻量,生产级召回仍需要租户与权限边界。

▸Skill 与 prompt 如何共享可信边界

技能加载、系统块和记忆事实并不是三套孤立机制;sources/commerce-agents/commerce-common/commerce_common/skills.py:42、sources/commerce-agents/commerce-common/commerce_common/prompt_assembly.py:103 与 sources/commerce-agents/commerce-common/commerce_common/memory.py:248 共同决定本轮哪些知识可以进入模型上下文。