fix: 修复 NameError/状态污染/类型标注/统计; 补全练习与 main; 新增 config/.gitignore/requirements; 文档统一
This commit is contained in:
@@ -68,19 +68,6 @@ class Message:
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemorySnapshot:
|
||||
"""记忆快照 - 用于保存和恢复状态"""
|
||||
state: Dict[str, Any] # 关键状态
|
||||
messages: List[Message] # 消息历史
|
||||
key_info: Dict[str, Any] # 关键信息摘要
|
||||
timestamp: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.timestamp:
|
||||
self.timestamp = datetime.now().isoformat()
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 第三部分:Working Memory(工作记忆)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Step 04 练习题:Memory 实战
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
🎯 练习目标:
|
||||
1. 理解三层记忆的协作方式
|
||||
2. 实现一个 Token 估算器
|
||||
3. 体验摘要压缩的副作用
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
"""
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 练习 1:Token 估算
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
"""
|
||||
任务:
|
||||
在 ShortTermMemory 上加一个 estimate_tokens() 方法,粗略估计当前占用的 token 数。
|
||||
|
||||
要求:
|
||||
1. 简单规则:1 个中文字符 ≈ 1.5 token,1 个英文单词 ≈ 1.3 token
|
||||
2. 对所有消息求和
|
||||
3. 返回 int(向上取整)
|
||||
|
||||
提示:
|
||||
- 正则分中英文:re.findall(r'[\u4e00-\u9fff]', text) 取汉字,剩下按空格分词
|
||||
- import math; math.ceil(...)
|
||||
"""
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 练习 2:基于 Token 阈值的自动压缩
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
"""
|
||||
任务:
|
||||
给 ShortTermMemory 加一个 maybe_compress(max_tokens: int) 方法:
|
||||
当 estimate_tokens() 超过 max_tokens 时,把较早的对话压缩成一行摘要,
|
||||
保留最近的 5 条。
|
||||
|
||||
要求:
|
||||
1. 触发时调用 summarize_older(keep_recent=5)
|
||||
2. 把摘要作为一个新的 Message(role="system", content=summary) 放回 messages 头部
|
||||
3. 删除被摘要覆盖的旧消息(避免 token 没降反升)
|
||||
"""
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 练习 3:把 MemorySystem 接到 SimpleAgent
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
"""
|
||||
任务:
|
||||
让 SimpleAgent 在 process() 时把每一轮对话写入 MemorySystem,
|
||||
并在下次决策前把 memory.get_context() 注入到 state['context'] 中。
|
||||
|
||||
要求:
|
||||
1. SimpleAgent.__init__ 里 new 一个 MemorySystem
|
||||
2. process() 末尾:self.memory.add_message('user' / 'assistant', ...)
|
||||
3. process() 开头:self.state['context'] = self.memory.get_context()
|
||||
|
||||
提示:
|
||||
- 直接修改 step_03/concept.py 是允许的(学习项目不是发布包)
|
||||
- 可以通过 Monkey-patching 避免破坏 step_03 原有行为
|
||||
"""
|
||||
|
||||
|
||||
def test_exercises():
|
||||
from step_04_memory.concept import MemorySystem
|
||||
|
||||
mem = MemorySystem()
|
||||
mem.add_message("user", "帮我生成销售月报")
|
||||
mem.add_message("assistant", "好的,请告诉我字段")
|
||||
print("上下文片段:")
|
||||
print(mem.get_context()[:200])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_exercises()
|
||||
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
Step 04 练习题答案
|
||||
|
||||
⚠️ 先自己思考,再看答案!
|
||||
⚠️ 答案不是唯一的,这里只是其中一种实现
|
||||
"""
|
||||
|
||||
import math
|
||||
import re
|
||||
|
||||
from step_04_memory.concept import MemorySystem, Message, ShortTermMemory
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 练习 1 答案:Token 估算
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
_CN_PATTERN = re.compile(r"[\u4e00-\u9fff]")
|
||||
_EN_WORD = re.compile(r"[A-Za-z]+")
|
||||
|
||||
|
||||
def estimate_tokens_for_text(text: str) -> int:
|
||||
cn = len(_CN_PATTERN.findall(text))
|
||||
en = len(_EN_WORD.findall(text))
|
||||
return math.ceil(cn * 1.5 + en * 1.3)
|
||||
|
||||
|
||||
def install_estimate_tokens() -> None:
|
||||
def estimate_tokens(self: ShortTermMemory) -> int:
|
||||
return sum(estimate_tokens_for_text(m.content) for m in self.messages)
|
||||
|
||||
ShortTermMemory.estimate_tokens = estimate_tokens
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 练习 2 答案:基于 Token 阈值的自动压缩
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def install_maybe_compress() -> None:
|
||||
def maybe_compress(self: ShortTermMemory, max_tokens: int = 800) -> bool:
|
||||
if not hasattr(self, "estimate_tokens"):
|
||||
raise RuntimeError("请先调用 install_estimate_tokens()")
|
||||
if self.estimate_tokens() <= max_tokens:
|
||||
return False
|
||||
summary = self.summarize_older(keep_recent=5)
|
||||
if not summary:
|
||||
return False
|
||||
# 保留最近 5 条,把摘要作为 system message 放最前
|
||||
recent = self.messages[-5:]
|
||||
self.messages = [Message(role="system", content=f"[历史摘要]\n{summary}")] + recent
|
||||
return True
|
||||
|
||||
ShortTermMemory.maybe_compress = maybe_compress
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 练习 3 答案:把 MemorySystem 接到 SimpleAgent
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def install_memory_to_agent() -> None:
|
||||
from step_03_simple_agent.concept import SimpleAgent
|
||||
|
||||
orig_init = SimpleAgent.__init__
|
||||
|
||||
def patched_init(self, *args, **kwargs):
|
||||
orig_init(self, *args, **kwargs)
|
||||
self.memory = MemorySystem()
|
||||
|
||||
SimpleAgent.__init__ = patched_init
|
||||
|
||||
orig_process = SimpleAgent.process
|
||||
|
||||
def patched_process(self, user_input: str) -> str:
|
||||
# 把记忆上下文注入 state
|
||||
self.state["context"] = self.memory.get_context()
|
||||
response = orig_process(self, user_input)
|
||||
# 记录本轮对话
|
||||
self.memory.add_message("user", user_input)
|
||||
self.memory.add_message("assistant", response)
|
||||
return response
|
||||
|
||||
SimpleAgent.process = patched_process
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 测试
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def test_answers():
|
||||
print("\n" + "=" * 60)
|
||||
print("Step 04 练习答案测试")
|
||||
print("=" * 60)
|
||||
|
||||
install_estimate_tokens()
|
||||
install_maybe_compress()
|
||||
|
||||
mem = MemorySystem()
|
||||
for i in range(20):
|
||||
mem.short_term.add("user", f"第 {i} 轮对话内容,包含中文与 english words " * 5)
|
||||
print(f"\n📝 练习 1: 注入 20 条后估算 token = {mem.short_term.estimate_tokens()}")
|
||||
compressed = mem.short_term.maybe_compress(max_tokens=200)
|
||||
print(f" maybe_compress() = {compressed}, 压缩后消息数 = {len(mem.short_term.messages)}")
|
||||
print(f" 压缩后估算 token = {mem.short_term.estimate_tokens()}")
|
||||
|
||||
print("\n📝 练习 3: SimpleAgent 接入 Memory")
|
||||
try:
|
||||
install_memory_to_agent()
|
||||
from step_03_simple_agent.concept import SimpleAgent
|
||||
|
||||
agent = SimpleAgent()
|
||||
agent.process("1 + 2")
|
||||
print(f" agent.memory 工作正常,消息数 = {len(agent.memory.short_term.messages)}")
|
||||
except Exception as e:
|
||||
print(f" 接入失败(可忽略,需在 step_03 父目录运行): {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_answers()
|
||||
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
Step 04: Memory - 记忆系统 主程序
|
||||
|
||||
运行方式:
|
||||
cd step_04_memory
|
||||
python main.py
|
||||
"""
|
||||
|
||||
from concept import demo
|
||||
|
||||
|
||||
def main():
|
||||
demo()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user