Essay

Agent Harness 工程:上下文压缩

By XiaoLeiJun

Agent Harness 工程:上下文压缩

上一篇我们把 Skill 拆成了一个按需加载机制:启动时只把 Skill 的 namedescription 放进系统提示词,等任务真的匹配某个 Skill 时,再调用 load_skill 加载完整 SKILL.md

这其实已经是在做一种上下文管理:不需要的内容,不要提前塞进上下文。

但 Agent 跑起来之后,还有一个更直接的问题:messages 会不断膨胀。

回顾一下前几篇的 Agent Loop:

  1. 用户发来任务;
  2. 模型返回工具调用;
  3. Harness 执行工具;
  4. 工具结果写回 messages
  5. 模型基于新的 messages 继续下一轮。

这个循环越跑越久,messages 里就会积累越来越多内容:

  • 用户需求;
  • 模型中间回复;
  • 工具调用参数;
  • 工具执行结果;
  • TodoWrite 更新;
  • Skill 正文;
  • Subagent 返回结果;
  • 权限拒绝原因;
  • 测试日志和错误栈;
  • 大段文件内容。

而模型的上下文窗口是有限的。超过窗口之后,轻则模型注意力下降,开始忘记关键约束;重则请求直接失败。

所以这一篇继续给 Harness 加一个能力:上下文压缩

本章仍然沿用前八章的同一个 Python Harness。为了让示例集中在核心问题,后文只展开与上下文管理直接相关的结构;工具 Schema 和通用分发代码继续使用前文定义。各代码块共同组成同一套 Harness,而不是彼此独立的实现。

先分清几个概念

在写代码之前,先把几个词分清楚。

上下文

上下文是模型在某一次调用里能看到的全部信息。

对我们的 Harness 来说,它通常包括:

  • 系统提示词;
  • 当前用户任务;
  • messages 里的历史对话;
  • 工具调用结果;
  • 当前 Todo;
  • 已加载的 Skill;
  • 压缩后的历史摘要;
  • 其它被注入本次请求的状态。

上下文是“这次调用能看见什么”。它不是一个固定文件,也不是一个永久存储,而是 Harness 在每次调用模型之前临时组装出来的一份输入。

比如同一个任务里,第一轮上下文可能只有用户需求和系统提示词;第五轮上下文里可能多了 Todo、工具结果、Skill 正文;第十轮上下文里可能又把早期工具结果压缩成摘要。

上下文窗口

上下文窗口是模型一次调用能接收的最大 token 数。

不同模型窗口大小不同。窗口越大,可以塞进去的内容越多,但这不意味着应该无脑塞。内容越杂,模型越难抓住重点;内容越长,成本也越高。

可以把上下文窗口理解成“这次模型调用的容量上限”。它只负责限制你最多能放多少东西,不会自动判断哪些信息重要。

记忆

记忆是跨轮次、跨任务、甚至跨会话保存下来的稳定信息。

比如:

  • 用户偏好的代码风格;
  • 项目的固定约束;
  • 某个长期任务的状态;
  • 已经确认过的事实;
  • 之前总结出的经验。

记忆不等于当前上下文。记忆可以被读取后放进上下文,但它本身更像一个外部状态库。

比如用户长期偏好“回答用中文”,这适合进入记忆;但某一次 pytest 的完整失败日志,只适合留在当前任务上下文里,甚至过几轮后就应该被摘要或 artifact 化。

messages 是什么

messages 是当前对话和工具执行过程的流水账。

它是上下文的一部分,但不是全部上下文;它也不是长期记忆。很多 messages 里的内容只在当时有用,比如一次 read_file 的完整输出、一次失败命令的完整日志。

这就是为什么我们需要压缩:不是所有进入过 messages 的内容,都值得永远留在上下文里。

上下文概念关系

messages 是上下文的来源之一;记忆在上下文外部,只有被检索出来后才进入本次模型输入。

可以用一句话区分这几个概念:

概念它是什么生命周期
上下文本次模型调用实际看到的输入每次调用前重新组装
上下文窗口模型允许输入的容量上限由模型决定
messages对话和工具调用过程的流水账当前任务内持续增长
记忆跨任务保存的稳定事实或偏好长期存在,按需放进上下文

上下文压缩要解决什么

上下文压缩不是简单地“删掉前面的消息”。

真正要做的是在有限窗口里保留最有价值的信息:

  • 当前用户目标;
  • 仍然有效的约束;
  • 当前 Todo;
  • 已经做过的重要决策;
  • 修改过哪些文件;
  • 工具调用得到的关键结论;
  • 失败过的路径和原因;
  • 最近几轮对话和工具结果。

同时丢掉或压缩低价值信息:

  • 很久以前的闲聊;
  • 已经用完的大段文件内容;
  • 重复的日志;
  • 已经总结过的工具输出;
  • 不再相关的中间尝试;
  • 过长但只需要结论的 Subagent 结果。
上下文压缩流程

上下文压缩不是让模型忘掉一切,而是把流水账整理成“摘要 + 近期细节 + 当前状态”。

上下文压缩难在哪里

上下文压缩听起来像是在做“长度优化”,但真正难的是判断信息价值。

第一,旧信息不一定不重要。 很久以前用户说过的一句“不要改 public API”,可能比最近一轮工具输出更重要。如果只按时间滑动窗口,很容易把关键约束丢掉。

第二,长内容不一定没价值。 大文件、大日志、大 diff 里可能只有一小段关键线索。直接截断会省 token,但也可能刚好截掉根因。

第三,摘要会损失细节。 LLM 摘要不是无损压缩。它可能漏掉边界条件,也可能把不确定的观察写得太确定。所以摘要里最好保留文件名、行号、命令、错误码、决策原因。

第四,消息协议不能被破坏。 工具调用有 assistant.tool_callstool 结果的配对关系。压缩时如果只删一半,模型接口可能直接报错。

第五,压缩本身也有成本。 调 LLM 做摘要会消耗 token 和时间。压缩太频繁,Agent 会变慢;压缩太晚,又可能已经超窗。

所以工程上通常不会只靠一种办法,而是分层处理:工具先限制输出,旧结果再 artifact 化,旧消息做摘要,最近几轮完整保留,最后再准备兜底压缩。

第一层:限制工具结果

最容易撑爆上下文的是工具结果。

尤其是 read_filebash、搜索类工具、测试命令。它们很容易一次返回几千行内容。

前几篇里,我们已经在一些工具里做了简单截断,比如 read_file 默认只读前 200 行。这是上下文压缩的第一层:工具返回时就不要返回无限大内容。

可以把 read_file 改成支持 start_linelimit

def read_file(path: str, start_line: int = 1, limit: int = 200) -> str:
    try:
        target = safe_path(path)
        if not target.exists():
            return f"Error: file does not exist - {path}"
        if not target.is_file():
            return f"Error: path is not a file - {path}"

        lines = target.read_text(encoding="utf-8").splitlines()
        start = max(1, start_line)
        end = min(len(lines), start + limit - 1)

        selected = lines[start - 1 : end]
        rendered = [f"{line_no}: {line}" for line_no, line in enumerate(selected, start)]

        if end < len(lines):
            rendered.append(f"... ({len(lines) - end} more lines)")

        return "\n".join(rendered)[:50000]
    except Exception as exc:
        return f"Error: {exc}"

这样模型不需要一次读完整文件。

它可以先读目录,再读文件开头,再根据需要读某个范围。对大文件来说,这比“一口气把全部内容塞进 messages”稳定得多。

同样,bash 也应该限制输出长度:

MAX_TOOL_OUTPUT_CHARS = 50000


def trim_tool_output(output: str) -> str:
    if len(output) <= MAX_TOOL_OUTPUT_CHARS:
        return output

    head = output[:20000]
    tail = output[-8000:]
    omitted = len(output) - len(head) - len(tail)

    return f"""
{head}

... ({omitted} characters omitted) ...

{tail}
""".strip()

这个策略很朴素,但非常有效。

它的原则是:工具函数本身就应该对返回值负责,不要把“无限输出”直接交给 Agent Loop。

第二层:替换已经用过的大工具结果

工具刚返回时,模型确实可能需要看完整结果。

但过了几轮之后,完整结果往往就没那么重要了。比如模型读过一个 500 行文件,接下来真正需要记住的可能只是:

  • 这个文件定义了哪个函数;
  • 关键配置在哪里;
  • 哪几行需要修改;
  • 是否发现了某个错误。

所以可以对旧的 tool 消息做压缩,把大段原始结果替换成摘要或占位符。

为了让代码更好理解,先定义一个 ContextState,把压缩相关的状态集中放在一起:

import hashlib
import json
from dataclasses import dataclass, field
from typing import Any


@dataclass
class ContextState:
    summary: str = ""
    tool_artifacts: dict[str, str] = field(default_factory=dict)
    token_budget: int = 24000
    recent_turns_to_keep: int = 6

这比把 CONTEXT_SUMMARYTOOL_ARTIFACTSCONTEXT_TOKEN_BUDGET 都散落成全局变量更清楚。

对一个任务来说,ContextState 就是 Harness 的上下文工作台:

字段作用
summary被压缩掉的旧历史摘要
tool_artifacts被移出上下文的大工具结果
token_budget本次模型输入的预算
recent_turns_to_keep最近完整保留多少个协议组

然后写一个确定性的工具结果压缩函数,不调用 LLM:

def compact_tool_message(
    message: dict[str, Any],
    state: ContextState,
    limit: int = 3000,
) -> dict[str, Any]:
    if message.get("role") != "tool":
        return message

    content = str(message.get("content") or "")
    if len(content) <= limit:
        return message

    digest = hashlib.sha256(content.encode("utf-8")).hexdigest()[:12]
    artifact_id = f"tool-result-{digest}"
    state.tool_artifacts[artifact_id] = content

    compacted = f"""
[large tool result compacted]
artifact_id: {artifact_id}
original_chars: {len(content)}
sha256: {digest}

head:
{content[:1200]}

tail:
{content[-800:]}
""".strip()

    return {**message, "content": compacted}

这段代码做了三件事:

  1. 如果工具结果很短,就不动;
  2. 如果工具结果很长,就把完整内容放进 state.tool_artifacts
  3. messages 里只保留 artifact_id、长度、hash、开头和结尾。

这里的 tool_artifacts 只是教学版内存存储。真实系统里可以把原始工具结果放到本地文件、对象存储、数据库或 run artifact 里。

只保存 artifact_id 还不够,Agent 必须能按范围重新读取。最小实现可以把下面的函数注册为只读工具,并由 Harness 注入当前 ContextState

def read_tool_artifact(
    state: ContextState,
    artifact_id: str,
    start: int = 0,
    limit: int = 3000,
) -> str:
    content = state.tool_artifacts.get(artifact_id)
    if content is None:
        return f"Error: artifact not found - {artifact_id}"

    start = max(0, start)
    limit = max(1, min(limit, 10000))
    chunk = content[start : start + limit]
    next_offset = start + len(chunk)
    suffix = f"\n[next_offset={next_offset}]" if next_offset < len(content) else ""
    return chunk + suffix

真实 Artifact Store 还要校验当前 run、Task 和 Agent 是否有权读取对应对象。artifact_id 是引用,不是授权凭证。

注意,这不是语义摘要,只是安全截断。它能防止上下文爆炸,但不保证保留所有关键含义。

语义摘要要到后面一层再做。

第三层:保留最近消息,摘要旧消息

对长期任务来说,最常用的压缩方式是:

  1. 最近几轮完整保留;
  2. 更早的消息合并成摘要;
  3. 摘要作为新的上下文状态放回模型输入。

先写一个粗略 token 估算函数:

def estimate_tokens(text: str) -> int:
    ascii_chars = sum(1 for char in text if ord(char) < 128)
    non_ascii_chars = len(text) - ascii_chars
    return max(1, ascii_chars // 4 + non_ascii_chars)


def message_to_text(message: dict[str, Any]) -> str:
    parts = [
        f"role: {message.get('role')}",
        f"content: {message.get('content') or ''}",
    ]

    if message.get("tool_calls"):
        parts.append(f"tool_calls: {message['tool_calls']}")

    return "\n".join(parts)


def estimate_messages_tokens(messages: list[dict[str, Any]]) -> int:
    return sum(estimate_tokens(message_to_text(message)) for message in messages)

这里仍然不是精确 tokenizer,但比统一使用 len(text) // 4 更保守:英文与代码粗略按 4 字符一个 token,中文等非 ASCII 字符按一个字符一个 token 预留空间。

生产系统最好用模型对应的 tokenizer,避免估算偏差太大。

接着定义压缩预算:

context_state = ContextState(
    token_budget=24000,
    recent_turns_to_keep=6,
)

token_budget 不应该等于模型最大窗口。

还要给模型输出、工具 schema、系统提示词、Skill 正文、临时插入的状态留空间。所以真实项目里通常会设置一个更保守的预算。

然后写摘要函数。

这里可以借助 LLM 对旧消息做语义摘要:

SUMMARY_PROMPT = """
你负责压缩 Agent Harness 的历史上下文。

请保留:
1. 用户当前目标和明确要求;
2. 已经完成的重要步骤;
3. 当前仍然有效的约束;
4. 修改过、读取过、需要继续关注的文件;
5. 工具调用得到的关键结论;
6. 失败过的命令、错误原因和已排除路径;
7. 当前 Todo 状态和下一步建议。

请丢弃:
1. 大段原始工具输出;
2. 重复日志;
3. 已经失效的中间尝试;
4. 和当前任务无关的细节。
"""


def update_context_summary(
    state: ContextState,
    old_messages: list[dict[str, Any]],
) -> None:
    payload = {
        "existing_summary": state.summary,
        "old_messages": old_messages,
    }

    response = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content": SUMMARY_PROMPT},
            {
                "role": "user",
                "content": json.dumps(payload, ensure_ascii=False, indent=2),
            },
        ],
        max_completion_tokens=2000,
    )

    choice = response.choices[0]
    if choice.finish_reason == "length":
        raise RuntimeError("context summary was truncated")

    summary = (choice.message.content or "").strip()
    if not summary:
        raise RuntimeError("context summary is empty")
    state.summary = summary[:8000]

这里把旧摘要也一起传进去,是为了做增量摘要。

否则每次压缩都会丢掉更早历史。

第四层:把压缩接进模型调用

现在写一个 ContextManager 风格的实现。

它做四件事:

  1. 先压缩过大的工具结果;
  2. 按消息协议把工具调用和结果组成不可拆分的组;
  3. 如果仍然超过预算,就把旧组摘要掉,并从当前历史中移除;
  4. 最后只把近期消息和摘要一起放进模型输入。
class MessageProtocolError(ValueError):
    pass


def group_protocol_turns(
    messages: list[dict[str, Any]],
) -> list[list[dict[str, Any]]]:
    """Keep an assistant tool call and all of its tool results together."""
    groups: list[list[dict[str, Any]]] = []
    index = 0

    while index < len(messages):
        message = messages[index]
        tool_calls = message.get("tool_calls") or []

        if message.get("role") == "assistant" and tool_calls:
            expected_ids = [str(call.get("id") or "") for call in tool_calls]
            if any(not item for item in expected_ids) or len(set(expected_ids)) != len(
                expected_ids
            ):
                raise MessageProtocolError("assistant tool calls need unique ids")

            group = [message]
            seen_ids: set[str] = set()
            index += 1

            while index < len(messages) and messages[index].get("role") == "tool":
                tool_message = messages[index]
                tool_call_id = str(tool_message.get("tool_call_id") or "")
                if tool_call_id not in expected_ids or tool_call_id in seen_ids:
                    raise MessageProtocolError(
                        f"unexpected tool result: {tool_call_id or 'missing id'}"
                    )
                group.append(tool_message)
                seen_ids.add(tool_call_id)
                index += 1

            if seen_ids != set(expected_ids):
                missing = sorted(set(expected_ids) - seen_ids)
                raise MessageProtocolError(f"missing tool results: {missing}")

            groups.append(group)
            continue

        if message.get("role") == "tool":
            raise MessageProtocolError("tool result has no preceding assistant call")

        groups.append([message])
        index += 1

    return groups


def split_for_compaction(
    messages: list[dict[str, Any]],
    keep_recent_turns: int,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
    groups = group_protocol_turns(messages)
    if len(groups) <= keep_recent_turns:
        return [], messages

    cutoff = len(groups) - keep_recent_turns
    old_messages = [message for group in groups[:cutoff] for message in group]
    recent_messages = [message for group in groups[cutoff:] for message in group]
    return old_messages, recent_messages


def compact_messages_if_needed(
    messages: list[dict[str, Any]],
    state: ContextState,
) -> list[dict[str, Any]]:
    # 结果压缩和历史裁剪都写回 canonical messages,避免下一轮重复处理旧历史。
    messages[:] = [compact_tool_message(message, state) for message in messages]

    summary_tokens = estimate_tokens(state.summary) if state.summary else 0
    if estimate_messages_tokens(messages) + summary_tokens <= state.token_budget:
        return messages

    old_messages, recent_messages = split_for_compaction(
        messages,
        keep_recent_turns=state.recent_turns_to_keep,
    )

    if old_messages:
        update_context_summary(state, old_messages)
        messages[:] = recent_messages

    return messages

这里必须原地更新 messages。如果只为当前请求返回一个裁剪副本,下一轮仍会再次看到完整旧历史,把同一批消息重复写进摘要;任务跑得越久,摘要反而越混乱。

再用一个很薄的对象统一暴露常规压缩、兜底压缩和当前摘要。后面的记忆、Prompt Assembler 与错误恢复都只依赖这个接口:

class ContextManager:
    def __init__(self, state: ContextState):
        self.state = state

    @property
    def summary(self) -> str:
        return self.state.summary

    def compact(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
        return compact_messages_if_needed(messages, self.state)

    def hard_compact(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
        messages[:] = hard_compact_messages(messages, self.state)
        return messages

然后构造真正发给模型的消息:

def build_model_messages(
    messages: list[dict[str, Any]],
    context_manager: ContextManager,
) -> list[dict[str, Any]]:
    # compact() 可能刚刚更新 summary,所以必须先压缩、后读取摘要。
    working_messages = context_manager.compact(messages)

    model_messages = [{"role": "system", "content": SYSTEM_PROMPT}]

    if context_manager.summary:
        model_messages.append(
            {
                "role": "system",
                "content": (
                    "下面是被压缩的历史上下文摘要:\n"
                    + context_manager.summary
                ),
            }
        )

    model_messages.extend(working_messages)
    return model_messages

最后在 run_loop() 里替换模型调用部分:

def run_loop(user_message: str, max_steps: int = 8) -> str:
    messages: list[dict[str, Any]] = [{"role": "user", "content": user_message}]
    context_manager = ContextManager(
        ContextState(token_budget=24000, recent_turns_to_keep=6)
    )

    for step in range(max_steps):
        response = client.chat.completions.create(
            model=MODEL,
            messages=build_model_messages(messages, context_manager),
            tools=TOOLS,
        )

        # 后面的工具调用处理逻辑继续沿用前几篇

这就是上下文压缩接入 Harness 的最小形态。

Agent Loop 仍然负责循环,工具系统仍然负责动作,Context Manager 负责在每次模型调用前整理输入。

上下文分层

上下文不是一整坨 messages,而是由稳定提示词、任务状态、摘要、近期消息和外部 artifacts 共同组成。

关于工具调用配对的一个坑

这里有一个很容易踩的坑。

Chat Completions 的工具调用消息有配对关系:

  1. assistant 消息里带 tool_calls
  2. 后面必须有对应 tool_call_idtool 消息。

如果压缩时只删掉其中一半,就可能破坏消息协议。

比如保留了一个 tool 消息,却删掉了前面的 assistant.tool_calls,模型接口可能直接报错。反过来,只保留 assistant.tool_calls,却删掉对应 tool 消息,也会出问题。

所以不要简单按消息条数硬切。

更稳的做法是把消息切成 turn:

  • 普通用户消息是一个 turn;
  • 普通 assistant 回复是一个 turn;
  • assistant tool_calls 加上后续所有对应 tool 结果是一组 turn;
  • 压缩时按 turn 整组保留或整组摘要。

上面的 group_protocol_turns() 已经实现了这个最小边界:它会校验调用 ID、拒绝孤立或重复的工具结果,并保证切分点不会落在工具调用组内部。流式响应还需要先完整聚合 tool_calls,通过 Schema 校验后再写入这里的 messages

读大文件怎么办

还有一个很实际的问题:一次读取了太多文件,直接把上下文窗口打满怎么办?

最好的方式不是“读完再压缩”,而是避免一开始就读太多。

可以给 Agent 几个更细的工具习惯:

先搜索,再读取。 比如先用 rg 找关键词、函数名、文件路径,再读取相关片段。

按范围读取。 read_file(path, start_line, limit) 比一次读全文件更稳。

工具结果带行号。 这样模型后续可以引用具体位置,也可以继续读取附近上下文。

必要时让 Subagent 调查。 大范围探索可以交给只读 Subagent,主 Agent 只接收结论。

把原始输出放 artifact。 主上下文里保留摘要和 artifact id,需要时再按范围重新读取。

这和 Skill 的按需加载是同一个原则:不要提前加载无关内容,也不要一次加载过多内容。

什么时候做压缩

压缩有几种触发时机。

每轮调用前检查。 这是最稳的方式。只要准备调用模型,就先估算上下文是否超预算。

工具结果写入后检查。 如果某次工具结果特别大,可以立刻压缩或转成 artifact。

Todo 完成一个阶段后检查。 比如一个 Todo 从 in_progress 变成 completed,可以把这一阶段的细节总结掉。

Subagent 返回后检查。 子代理结果本来就应该是压缩结果,但如果仍然很长,也可以继续摘要。

异常兜底。 如果模型接口返回上下文超限错误,就触发更激进的压缩,再重试一次。

教学版可以先用“每轮调用前检查 + 异常兜底”。

兜底压缩

即使前面做了很多限制,也可能遇到上下文仍然超限的情况。

比如用户一次贴了超长文本,或者多个工具连续返回大量内容。

这时需要一个兜底策略:

  1. 保留系统提示词;
  2. 保留当前用户最新请求;
  3. 保留当前 Todo;
  4. 保留压缩摘要;
  5. 保留最近几条消息;
  6. 其它全部摘要或丢弃。

可以写一个更激进的 fallback:

def hard_compact_messages(
    messages: list[dict[str, Any]],
    state: ContextState,
) -> list[dict[str, Any]]:
    if not messages:
        return []

    compacted = [compact_tool_message(message, state) for message in messages]
    old_messages, recent_messages = split_for_compaction(
        compacted,
        keep_recent_turns=3,
    )
    if old_messages:
        update_context_summary(state, old_messages)

    latest_user = next(
        (message for message in reversed(compacted) if message.get("role") == "user"),
        None,
    )

    if latest_user and all(message is not latest_user for message in recent_messages):
        return [latest_user] + recent_messages

    return recent_messages

这会丢细节,但能让任务继续跑。

上下文压缩永远有取舍:压缩得越狠,越省 token,但越容易丢信息。

所以更好的做法是尽量在前面几层就控制住,而不是每次都靠兜底。

压缩一定会丢失信息

这是上下文压缩必须面对的事实。

无论是截断、摘要、替换、滑动窗口,都会丢掉一些细节。

所以不要把上下文压缩理解成“无损压缩”。它更像是做笔记:

  • 原始工具输出是现场录音;
  • 摘要是会议纪要;
  • Todo 是行动清单;
  • Skill 是做事方法;
  • Memory 是长期档案。

会议纪要能帮助你继续工作,但它不可能保留现场录音的全部细节。

工程上要做的是让丢失尽量可控:

  • 重要事实写进摘要;
  • 关键文件和行号保留;
  • 原始大结果放 artifact;
  • 当前 Todo 不压掉;
  • 最近几轮完整保留;
  • 压缩发生时留下日志。

小结

这一篇给 Agent Harness 增加了上下文压缩层:

  1. 区分上下文、上下文窗口、记忆和 messages
  2. 在工具返回时限制输出长度;
  3. 对旧的大工具结果做替换或 artifact 化;
  4. 按完整工具调用组保留近期消息,旧消息用 LLM 做增量摘要;
  5. 在模型调用前通过 build_model_messages() 统一整理上下文;
  6. 原地推进 canonical messages,避免同一段历史被重复摘要;
  7. 准备一个更激进的兜底压缩策略;
  8. 接受压缩会丢细节,并用日志、artifact、Todo 和摘要降低风险。

到这里,Agent Harness 已经开始有了长期运行的基础能力:不是只会一轮一轮地堆 messages,而是能主动整理自己的工作上下文。

下一章我们就把“记忆”单独展开:哪些信息应该进入长期记忆,哪些只应该留在当前任务里,Memory Store 应该怎么设计,又如何避免把错误事实永久保存下来。