news 2026/9/28 11:29:29

第89篇:Vibe Coding时代:LangGraph 长期记忆实战,用 config.toml 骨架让 Agent 记住项目约定与用户偏好

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第89篇:Vibe Coding时代:LangGraph 长期记忆实战,用 config.toml 骨架让 Agent 记住项目约定与用户偏好

1. 为什么你的 LangGraph Agent 总是“失忆”

如果你正在用 LangGraph 搭 Coding Agent,大概率遇到过这种场景:昨天刚跟它强调过“所有接口返回必须走统一响应体”,今天新开一个会话,它又给你写了个裸return {"name": "x"}。你不得不把项目约定、分支命名规范、测试目录规则、PR 模板、用户偏好的代码风格,一遍又一遍地塞进 Prompt。这不是模型笨,而是它没有长期记忆。

Vibe Coding 的核心体验是“用自然语言驱动开发”,但自然语言驱动的前提是 Agent 得记住上下文之外那些稳定不变的东西。短期任务状态可以放在 State 里随会话销毁,可项目约定、团队规范、用户偏好这些信息是跨会话、跨任务长期有效的。如果每次都要用户重新输入或让模型从文档里重新推断,效率低、易遗漏,还容易在长链路里被冲掉。

这篇要解决的就是这件事:给 LangGraph Agent 加一层可治理的长期记忆,用config.toml作为配置骨架,把记忆的存储、检索、注入串成一条稳定链路。适合正在做 Coding Agent、代码助手、自动化研发流程的开发者,也适合想把团队规范沉淀进 Agent 的小团队。下面所有代码和配置都可以直接复制运行,我会用 TaoToken 的统一 Key/API 通道接入模型,省去多平台切换的麻烦。

2. TaoToken 前置:统一 Key 与 API 通道

在写记忆逻辑之前,先把模型调用通道固定下来。LangGraph 本身不绑定模型供应商,但ChatOpenAI这类封装需要一个兼容 OpenAI 协议的 endpoint。TaoToken 提供统一的 API 通道,一个 Key 就能覆盖对话、编码等场景,适合在 Agent 项目里做统一接入。

你需要先拿到一个 API Key。打开控制台创建 Key:

https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite

创建完成后,Key 只在创建时完整显示一次,复制保存好。接入文档在这里,里面有 base_url 和鉴权方式的说明:

https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite

如果你只是想先验证模型通道是否通,可以直接在模型对话页试一句:

https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite

长期跑编码任务、Agent 工作流的话,Coding Plan 更适合,额度模型和调用方式在页面里有说明:

https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite

API 的基础地址是https://taotoken.net/api,注意这个地址不带任何查询参数,直接作为base_url使用。Key 的管理入口在:

https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite

把 Key 写进环境变量,不要硬编码进代码:

export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"

这样后面config.toml里只引用环境变量名,代码和配置分离,团队协作时也不会把 Key 提交到仓库。

3. 用 config.toml 搭记忆骨架

长期记忆最容易失控的地方不是存储,而是“什么该记、记多久、谁能改”。如果这些规则散落在代码里,后面治理会很痛苦。所以第一步是把记忆策略抽到config.toml,让配置成为唯一事实来源。

项目结构建议这样组织:

long-memory-agent/ ├── config.toml ├── app.py ├── graph.py ├── state.py ├── memory_store.py ├── chains.py └── requirements.txt

依赖安装:

pip install langchain langchain-openai langgraph python-dotenv tomli

config.toml的内容如下,每一段都对应一个可治理的维度:

[model] provider = "openai-compatible" base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" model_name = "gpt-4o-mini" temperature = 0.1 [memory] store_path = "memories.jsonl" default_status = "active" max_inject = 5 min_score = 1 [memory.scope] require_team_id = true require_project_id = true [memory.types] allowed = [ "coding_style", "testing_rule", "dependency_rule", "branch_rule", "pr_template", "user_preference", "failure_lesson", ] [memory.governance] require_source = true allow_model_guess = false sensitive_keywords = ["password", "secret", "token", "private_key"]

这里有几个关键设计。max_inject控制每次注入 Prompt 的记忆条数,避免 Prompt 噪声;min_score是检索的最低命中分,低于它的记忆不注入;allowed白名单限定只有这些类型能进长期记忆,模型自己猜的内容不在白名单里,直接拒绝;sensitive_keywords做一层敏感词拦截,防止密钥、密码被误存。

读取配置的代码放在memory_store.py顶部:

import json import time import uuid import tomli from pathlib import Path CONFIG_PATH = Path(__file__).parent / "config.toml" with CONFIG_PATH.open("rb") as f: CONFIG = tomli.load(f) MEMORY_FILE = Path(__file__).parent / CONFIG["memory"]["store_path"] MEMORY_FILE.touch(exist_ok=True)

这样改策略不用动代码,改config.toml重启即可。团队里谁负责记忆治理,谁就维护这个文件,边界清晰。

4. 记忆存储与检索:写入、召回、注入

存储层要解决三件事:写入时校验、检索时打分、注入时裁剪。先看写入函数,它会在落盘前做白名单和敏感词检查:

def save_memory( team_id: str, project_id: str, memory_type: str, content: str, tags: list[str], source: str, ) -> str: allowed = CONFIG["memory"]["types"]["allowed"] if memory_type not in allowed: raise ValueError(f"memory_type 不在白名单:{memory_type}") if CONFIG["memory"]["governance"]["require_source"] and not source: raise ValueError("source 不能为空") lowered = content.lower() for kw in CONFIG["memory"]["governance"]["sensitive_keywords"]: if kw in lowered: raise ValueError(f"内容命中敏感词,拒绝写入:{kw}") item = { "id": str(uuid.uuid4()), "team_id": team_id, "project_id": project_id, "memory_type": memory_type, "content": content, "tags": tags, "source": source, "created_at": int(time.time()), "status": CONFIG["memory"]["default_status"], } with MEMORY_FILE.open("a", encoding="utf-8") as f: f.write(json.dumps(item, ensure_ascii=False) + "\n") return item["id"]

检索函数按 team 和 project 隔离,再做关键词打分,最后按max_inject截断:

def search_memories(team_id: str, project_id: str, query: str) -> list[dict]: memories = [] for line in MEMORY_FILE.read_text(encoding="utf-8").splitlines(): if not line.strip(): continue item = json.loads(line) if ( item["team_id"] == team_id and item["project_id"] == project_id and item["status"] == "active" ): memories.append(item) scored = [] for item in memories: text = " ".join([item["content"], " ".join(item["tags"]), item["memory_type"]]).lower() score = sum(1 for token in query.lower().split() if token in text) if score >= CONFIG["memory"]["min_score"]: scored.append((score, item)) scored.sort(key=lambda x: x[0], reverse=True) return [item for _, item in scored[: CONFIG["memory"]["max_inject"]]]

注入环节放在chains.py,把命中的记忆拼进 system prompt,并明确要求模型说明哪些约定影响了方案:

import os from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser llm = ChatOpenAI( model=CONFIG["model"]["model_name"], temperature=CONFIG["model"]["temperature"], base_url=CONFIG["model"]["base_url"], api_key=os.environ[CONFIG["model"]["api_key_env"]], ) parser = StrOutputParser() def generate_plan_with_memory(requirement: str, memories: list[dict]) -> str: prompt = ChatPromptTemplate.from_messages([ ("system", "你是资深工程师。请根据用户需求和项目长期记忆生成实现计划。"), ("user", "用户需求:{requirement}\n\n项目长期记忆:{memories}\n\n请生成实现计划,并明确说明哪些项目约定影响了本次方案。"), ]) return (prompt | llm | parser).invoke({ "requirement": requirement, "memories": memories, })

注意base_url和api_key都从config.toml和环境变量读取,没有硬编码。这样换模型通道只改配置,不动业务代码。

5. 验证一次记忆写入与召回

先初始化几条项目记忆,模拟团队规范沉淀:

from memory_store import save_memory save_memory( team_id="team_a", project_id="backend", memory_type="coding_style", content="所有 API 响应必须使用统一格式:{'code': int, 'message': str, 'data': object}。", tags=["api", "response", "style"], source="manual", ) save_memory( team_id="team_a", project_id="backend", memory_type="testing_rule", content="新增接口必须补充 tests/ 下对应 pytest 测试,测试文件命名为 test_xxx.py。", tags=["pytest", "testing"], source="manual", ) save_memory( team_id="team_a", project_id="backend", memory_type="dependency_rule", content="除非经过人工批准,不允许 Agent 自动新增第三方依赖。", tags=["dependency", "approval"], source="manual", )

然后定义 State 和 Graph:

from typing import TypedDict, List, Dict class LongMemoryState(TypedDict): team_id: str project_id: str requirement: str related_memories: List[Dict] generated_plan: str errors: List[str] final_answer: str
from langgraph.graph import StateGraph, END from state import LongMemoryState from memory_store import search_memories from chains import generate_plan_with_memory def retrieve_memory_node(state: LongMemoryState) -> LongMemoryState: try: state["related_memories"] = search_memories( team_id=state["team_id"], project_id=state["project_id"], query=state["requirement"], ) except Exception as e: state["errors"].append(f"检索长期记忆失败:{str(e)}") return state def plan_node(state: LongMemoryState) -> LongMemoryState: try: state["generated_plan"] = generate_plan_with_memory( requirement=state["requirement"], memories=state["related_memories"], ) except Exception as e: state["errors"].append(f"生成计划失败:{str(e)}") return state def final_node(state: LongMemoryState) -> LongMemoryState: state["final_answer"] = ( "## 命中的长期记忆\n\n" f"```json\n{state['related_memories']}\n```\n\n" "## 生成计划\n\n" f"{state['generated_plan']}\n\n" "## 错误信息\n\n" f"{state['errors']}" ) return state def build_graph(): graph = StateGraph(LongMemoryState) graph.add_node("retrieve_memory", retrieve_memory_node) graph.add_node("plan", plan_node) graph.add_node("final", final_node) graph.set_entry_point("retrieve_memory") graph.add_edge("retrieve_memory", "plan") graph.add_edge("plan", "final") graph.add_edge("final", END) return graph.compile()

入口app.py:

from graph import build_graph def main(): app = build_graph() state = { "team_id": "team_a", "project_id": "backend", "requirement": "新增用户列表接口,并返回统一响应格式,同时补充测试", "related_memories": [], "generated_plan": "", "errors": [], "final_answer": "", } result = app.invoke(state) print(result["final_answer"]) if __name__ == "__main__": main()

运行python app.py,预期输出里应该命中三条记忆:统一响应格式、pytest 测试规则、依赖审批规则。生成计划里会明确写出返回{'code':0,'message':'success','data':...}、新增tests/test_user.py、不随意新增依赖。如果命中为空,先检查team_id和project_id是否和写入时一致,这是最常见的隔离问题。

6. 本篇常见错排查

报错一:tomli导入失败。Python 3.11 以上内置tomllib,可以直接把import tomli换成import tomllib as tomli,读取方式不变。低于 3.11 就装tomli。

报错二:api_key读取为 None。检查环境变量名是否和config.toml里的api_key_env一致。os.environ[CONFIG["model"]["api_key_env"]]用的是方括号,变量不存在会直接 KeyError,这比静默失败更好排查。

报错三:检索结果为空。三个原因最常见:team_id/project_id不匹配、status不是active、query 分词后没有命中任何 token。可以先把min_score临时改成 0 看是否能召回,确认是打分问题还是隔离问题。

报错四:写入被敏感词拦截。如果记忆内容里确实包含token这类词但并非密钥,可以调整sensitive_keywords列表,或者把该词从列表移除。但不要为了省事直接清空列表,那等于放弃这层防护。

报错五:Prompt 里记忆太多导致模型跑偏。调小max_inject,比如从 5 改成 3,同时提高min_score。记忆不是越多越好,精准命中比全量注入更有效。

报错六:模型把猜测写进了记忆。检查allow_model_guess是否为false,并且所有写入都走save_memory的白名单校验。不要让模型输出直接落盘,必须经过人工确认或管理员配置。

7. 长期记忆治理与下一步

记忆能写能读只是第一步,真正决定 Agent 能不能长期服务一个团队的是治理能力。config.toml里已经预留了status、source、allowed这些字段,接下来可以补上更新、禁用、删除三个操作。禁用比删除更安全,把status改成disabled,检索时自动跳过,历史记录还在,方便审计。

一个实用的经验是:每次 Agent 生成计划后,把“本次命中了哪些记忆”打印出来,人工扫一眼。如果发现某条记忆被频繁命中但内容已经过时,就把它禁用并写入新版本。这样记忆库会随着项目演进保持干净,而不是越积越乱。

如果你想把这条链路跑在更稳定的编码通道上,可以用 Coding Plan 的额度来跑 Agent 工作流,模型调用和记忆治理分开管理,互不干扰:

https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite

Key 的创建和管理统一在控制台完成,接入文档里有完整的 base_url 和参数说明。把config.toml里的base_url指向https://taotoken.net/api,环境变量里放好 Key,整套记忆链路就能直接跑起来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 11:28:54

【MyBatis系列3】MyBatis SQL执行流程

主要讲解MyBatis中SQL的执行流程,基于MyBatis的基础知识进行更深层次的剖析。前言在《【MyBatis系列1】基础知识(上)》中,我们讲解了MyBaits的工作原理,以及它的四大核心组件的使用姿势,包括SqlSessionFact…

作者头像 李华