跳到主要内容

MemGPT 源码阅读

· 阅读需 29 分钟

1. 整体架构概览

1.1 MemGPT的核心模块及其职责

graph LR
A[agent] --> B[agent_store]
A --> C[data_types]
A --> D[embeddings]
A --> E[functions]
A --> F[memory]
A --> G[models]
A --> H[persistence_manager]
A --> I[prompts]
A --> J[server]
  • agent: Agent的核心逻辑,包括初始化、处理消息、状态管理等
  • agent_store: Agent状态的持久化存储,支持不同的后端(如PostgreSQL、Chroma等)
  • data_types: MemGPT使用的核心数据结构,如AgentState、Message、Passage等
  • embeddings: 封装embedding相关逻辑,如文本分块、向量化等
  • functions: 自定义函数的定义、注册与调用
  • memory: 包括CoreMemory、RecallMemory和ArchivalMemory,分别对应不同层次的记忆
  • models: 定义了与LLM交互的请求与响应格式
  • persistence_manager: 管理Agent状态在不同存储后端间的持久化
  • prompts: 包含了一些默认的prompt模板
  • server: 提供了将Agent封装为RESTful API服务的能力

1.2 模块交互与数据流

sequenceDiagram
participant C as Client
participant S as Server
participant A as Agent
participant M as Memory
participant P as PersistenceManager
participant L as LLM

C->>S: 发送消息
S->>A: 将消息传递给Agent
A->>M: 查询/更新 Memory
A->>P: 持久化状态变更
A->>L: 发送消息序列,请求LLM响应
L-->>A: 返回LLM响应
A->>A: 解析LLM响应(含函数调用)
A-->>S: 返回最终回复
S-->>C: 返回最终回复
  1. Client通过Server将消息发送给Agent
  2. Agent查询/更新内存,并通过PersistenceManager持久化状态变更
  3. Agent将消息历史发送给LLM,请求下一步响应
  4. LLM返回响应,其中可能包含函数调用
  5. Agent解析LLM响应,必要时进行函数调用,最终生成回复
  6. Agent通过Server将最终回复返回给Client

2. Agent的状态管理

2.1 Agent类的设计与实现

Agent类(agent.py)是MemGPT的核心,它封装了一个Agent的完整状态和交互逻辑。其主要组成部分包括:

  • config: Agent的配置,如LLM/Embedding设置、所使用的memory/function等
  • memory: Agent的核心记忆,包括personahuman部分
  • functions: Agent可调用的函数定义
  • messages: Agent的历史消息记录

Agent类的核心方法是step(),它接受一条用户消息,经过一系列处理步骤,最终生成Agent的回复:

  1. 将用户消息添加到messages
  2. 构造prompt(包括systemmemoryfunctionsmessages等),发送给LLM
  3. 解析LLM响应,提取函数调用回复内容
  4. 如果存在函数调用,则执行对应的Python函数,将函数结果添加到messages
  5. 如有必要,总结messages以控制token数量
  6. 将Agent回复添加到messages
  7. 根据需要持久化状态变更(通过PersistenceManager)

2.2 PersistenceManager与存储后端

PersistenceManager抽象类(persistence_manager.py)定义了一组接口,用于管理Agent状态在不同存储后端间的持久化。

目前主要的实现是LocalStateManager,它将状态直接维护在内存中,主要用于demo/debug场景。对于生产环境,则需要实现基于数据库的PersistenceManager,将状态持久化到数据库中。

PersistenceManager的主要方法包括:

  • append_messages(): 添加新消息
  • trim_messages(): 从头部删除部分消息
  • swap_system_message(): 更换system消息
  • update_memory(): 更新memory内容

通过PersistenceManager,可以将Agent的状态变更同步到底层存储,并在Agent重启后恢复之前的状态。

2.3 AgentState等核心数据结构

AgentState(data_types.py)封装了一个Agent的完整状态,包括其配置、初始prompt、函数定义、消息历史等。AgentState主要用于在Agent和持久化存储之间传递状态。

其他一些核心数据结构还包括:

  • Message: 表示Agent发送或接收的一条消息,包括role(system/assistant/user)、contentfunction_call等字段
  • Passage: 表示一段用于语义检索的文本片段,包括textembedding等字段
  • LLMConfig/EmbeddingConfig: 封装LLM和Embedding的配置,如modelapi_key

2.4 Agent运行过程中的状态变迁

下面以一个典型的场景为例,展示Agent运行过程中的状态变迁:

stateDiagram-v2
[*] --> Idle: 创建Agent
Idle --> WaitingForUserMessage: 等待用户消息
WaitingForUserMessage --> ProcessingUserMessage: 收到用户消息
ProcessingUserMessage --> WaitingForLLMResponse: 发送消息给LLM
WaitingForLLMResponse --> ProcessingLLMResponse: 收到LLM响应
ProcessingLLMResponse --> CallingFunction: 调用函数
CallingFunction --> ProcessingLLMResponse: 函数执行完毕
ProcessingLLMResponse --> GeneratingReply: 生成回复
GeneratingReply --> PersistingState: 持久化状态变更
PersistingState --> WaitingForUserMessage: 等待下一条用户消息
  1. Agent创建后处于Idle状态,等待用户消息
  2. 收到用户消息后,进入ProcessingUserMessage状态,将消息添加到messages
  3. 发送消息序列给LLM,进入WaitingForLLMResponse状态
  4. 收到LLM响应后,进入ProcessingLLMResponse状态,解析响应
  5. 如果响应中包含函数调用,则进入CallingFunction状态,执行对应函数
  6. 函数执行完毕后,回到ProcessingLLMResponse状态
  7. 生成最终回复,进入GeneratingReply状态
  8. 持久化状态变更(messagesmemory等),进入PersistingState状态
  9. 状态持久化完成后,回到WaitingForUserMessage状态,等待下一条用户消息

3. 记忆管理

3.1 三种类型Memory的作用与实现

MemGPT将Agent的记忆分为三个层次:

  • CoreMemory(memory.py): 对应Agent prompt中直接出现的memory部分,通常包含persona(Agent的自我认知)和human(用户信息)两个部分。CoreMemory直接参与对话,但受token数量限制,通常只能保留最核心的信息。

  • RecallMemory(memory.py): 对应Agent的历史对话记录,可以通过文本搜索和时间范围查询来检索。RecallMemory不直接出现在prompt中,但Agent可以通过conversaction_search等函数来访问其内容。RecallMemory的实现基于文档数据库如PostgreSQL

  • ArchivalMemory(memory.py): 对应Agent的长期记忆,可以通过语义检索来访问。ArchivalMemory也不直接出现在prompt中,但Agent可以通过archival_memory_search等函数来访问其内容。ArchivalMemory的实现基于向量数据库如ChromaPinecone等,将文本片段映射为向量,支持高效的相似度检索。

在对话过程中,CoreMemory主要用于维护Agent的状态,而RecallMemoryArchivalMemory则用于知识检索和问题回答。

3.2 向量数据库在RecallMemory与ArchivalMemory中的应用

RecallMemoryArchivalMemory在检索时都用到了向量数据库:

  • 对于RecallMemory,每条消息在插入时都会提取一个embedding向量,以支持后续的语义检索。检索时,输入文本也被转换为向量,然后在向量空间中寻找最相似的消息。

  • 对于ArchivalMemory,外部知识库的文档会被切分成小段落(passage),每个段落提取一个embedding向量。当Agent需要访问这些知识时,会将问题转换为向量,在ArchivalMemory中检索出最相关的passage。

向量数据库在这里的作用,就是将非结构化的文本转换为结构化的向量表示,使得语义检索变得高效。常见的向量数据库包括PineconeWeaviateChroma等。MemGPT主要使用Chroma,但也提供了一套抽象接口StorageConnector,允许接入其他向量数据库。

3.3 CoreMemory如何参与prompt的构建

下面是一个简化版的prompt构建过程,展示了CoreMemory是如何参与其中的:

def construct_prompt(
system: str,
memory: CoreMemory,
messages: List[Message],
functions: List[Function]
) -> str:
prompt = f"""
{system}

### Memory
<persona>
{memory.persona}
</persona>

<human>
{memory.human}
</human>

### Messages
{format_messages(messages)}

### Functions
{format_functions(functions)}

### Response
"""

return prompt

可以看到,CoreMemory中的personahuman字段,会被直接插入到prompt的Memory部分。这使得CoreMemory的内容能够直接影响Agent的行为。

RecallMemoryArchivalMemory则不直接出现在prompt中,它们主要通过函数调用的方式(conversaction_searcharchival_memory_search等)来提供支持。

4. 函数化交互

4.1 函数的定义与自动schema生成

在MemGPT中,函数以Python函数的形式定义,位于functions目录下。例如一个简单的搜索函数:

def search_wikipedia(query: str) -> str:
"""Search Wikipedia for information."""
search_quality_reflection = "The original query could not be satisfactorily answered by searching Wikipedia. Additional information from other sources may be needed."
search_quality_score = 2

results = wikipedia.search(query)

if not results:
return f"No results found for query: {query}. {search_quality_reflection}"

try:
summary = wikipedia.summary(results[0], sentences=3)
search_quality_score = 4
search_quality_reflection = "The Wikipedia search provided relevant information to help answer the original query. However, additional details or clarification may still be needed."
except wikipedia.exceptions.PageError:
summary = f"No page found for {results[0]}. {search_quality_reflection}"
except wikipedia.exceptions.DisambiguationError as e:
summary = f"Multiple pages found for {results[0]}: {e.options}. {search_quality_reflection}"

return f"{summary}\nSearch quality score: {search_quality_score}\nSearch quality reflection: {search_quality_reflection}"

MemGPT会自动分析这个函数的签名,提取出输入参数(query)和返回值类型(str),生成对应的JSON Schema:

{
"name": "search_wikipedia",
"description": "Search Wikipedia for information.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Query to search Wikipedia for."
}
},
"required": ["query"]
}
}

这个Schema描述了函数的名称、描述、参数列表等信息,可以直接传给LLM,使其了解如何调用该函数。

4.2 函数的注册与调用流程

函数注册的入口是functions.py中的load_all_function_sets():

  1. 扫描functions目录,导入所有的Python文件
  2. 对每个文件,调用load_function_set()提取其中定义的函数
    • 使用inspect模块分析函数签名
    • 调用generate_schema()生成JSON Schema
    • 将函数和Schema打包成FunctionSet
  3. 将所有FunctionSet合并成一个字典,供Agent使用

Agent在收到LLM的函数调用请求后,会经历以下步骤:

  1. 解析请求,提取函数名和参数
  2. functions字典中查找对应的函数定义
  3. 使用inspect模块动态调用该函数,传入解析出的参数
  4. 获取函数返回值,添加到消息流中,发送给LLM

这个过程可以简化为:

sequenceDiagram
LLM->>Agent: 函数调用请求
Agent->>FunctionRegistry: 查找函数定义
FunctionRegistry-->>Agent: 返回函数定义
Agent->>Function: 动态调用
Function-->>Agent: 返回执行结果
Agent->>LLM: 添加到消息流

4.3 函数在Agent交互中的作用

函数大大增强了Agent的能力,使其可以访问和操作外部信息。一些典型的应用场景包括:

  • 知识检索: 通过archival_memory_search等函数,Agent可以从自己的知识库中检索信息来回答问题。
  • 数据查询: Agent可以通过函数从数据库、API等外部数据源获取数据。
  • 工具集成: Agent可以通过函数调用外部工具,如搜索引擎、计算器、翻译服务等。
  • 环境交互: 在某些应用场景下,Agent还可以通过函数来影响其所在的环境,如控制智能家居设备等。

下面是一个函数在Agent对话中发挥作用的例子:

User: What is the capital of France?

Assistant: To find the capital of France, I will perform a Wikipedia search.
<search_quality_reflection>
The Wikipedia search should provide the needed information to answer this query directly.
</search_quality_reflection>
<search_quality_score>5</search_quality_score>

<function_call>
{
"name": "search_wikipedia",
"arguments": {
"query": "capital of France"
}
}
</function_call>

<search_result>
Paris is the capital and most populous city of France, with an estimated population of 2,165,423 residents in 2019 in an area of more than 105 square kilometres (41 square miles). Since the 17th century, Paris has been one of Europe's major centres of finance, diplomacy, commerce, fashion, gastronomy, science, and arts. The City of Paris is the centre and seat of government of the region and province of Île-de-France, or Paris Region, which has an estimated population of 12,174,880, or about 18 percent of the population of France as of 2017.
</search_result>

<search_quality_reflection>
The Wikipedia search provided a clear and direct answer to the question, giving relevant information about Paris being the capital of France along with additional details like population and significance. I believe I can answer the original query satisfactorily with this information.
</search_quality_reflection>
<search_quality_score>5</search_quality_score>

<result>
Paris is the capital of France. It is the country's most populous city, with an estimated population of over 2.1 million residents as of 2019. Paris has been a major center of finance, diplomacy, commerce, fashion, science and the arts in Europe since the 17th century. It is the seat of government for the Île-de-France region.
</result>

这个例子展示了函数如何帮助Agent回答问题:

  1. Agent收到一个关于法国首都的问题
  2. Agent决定通过search_wikipedia函数来查找答案
  3. search_wikipedia函数调用Wikipedia API搜索"capital of France"
  4. 函数返回包含答案的搜索结果摘要
  5. Agent将搜索结果添加到消息流中
  6. 基于搜索结果,Agent生成最终答案并返回给用户

可以看到,函数使Agent能够访问自己知识范围以外的信息,大大提升了其问答能力。同时,函数也为Agent提供了一种"思考"的方式,它可以在消息流中记录自己的函数调用过程和结果判断,使其推理过程更加清晰和可解释。

5. 服务化

5.1 RESTful API的设计

MemGPT提供了一套RESTful API,将Agent的各项能力以Web服务的形式暴露出来。其主要接口包括:

  • /agents: Agent的创建和查询
  • /agents/{agent_id}/config: Agent配置的查询和修改
  • /agents/{agent_id}/memory: Agent记忆的查询和修改
  • /agents/{agent_id}/messages: Agent消息的发送和查询

这些接口涵盖了对Agent的主要操作,使得Agent能够方便地集成到各种应用程序中。

API的设计遵循了RESTful的原则:

  • 使用URL来标识资源,如/agents/{agent_id}表示一个特定的Agent
  • 使用HTTP方法来表示操作,如GET表示查询,POST表示创建,PUT表示更新等
  • 使用JSON作为数据交换格式
  • 无状态,每个请求都包含了完成操作所需的所有信息

5.2 FastAPI的应用

MemGPT使用FastAPI框架来实现Web服务。FastAPI是一个基于Python 3.6+的高性能Web框架,它有以下特点:

  • 基于标准的Python类型提示,容易编写和维护
  • 自动生成API文档(Swagger UI和ReDoc)
  • 高性能,可与NodeJS和Go媲美
  • 支持异步请求处理

在MemGPT中,FastAPI主要用于:

  1. 定义API路由和处理函数。例如:
@router.post("/agents", response_model=CreateAgentResponse)
def create_agent(
request: CreateAgentRequest,
user_id: UUID = Depends(get_current_user)
):
# create agent logic here
  1. 请求参数和响应数据的序列化和验证。例如:
class CreateAgentRequest(BaseModel):
config: Dict[str, Any] = Field(...)

class CreateAgentResponse(BaseModel):
agent_id: UUID = Field(...)
  1. 依赖注入。例如:
async def get_current_user(token: str = Depends(oauth2_scheme)):
# authenticate user logic here
  1. 异常处理。例如:
@app.exception_handler(AgentNotFoundError)
async def agent_not_found_handler(request: Request, exc: AgentNotFoundError):
return JSONResponse(
status_code=404,
content={"message": f"Agent {exc.agent_id} not found"},
)

通过FastAPI,MemGPT可以用较少的代码实现一个功能完善、文档完备、性能高效的Web服务。

5.3 Server核心逻辑

SyncServer类(server.py)实现了MemGPT服务的核心逻辑,主要包括:

  1. Agent管理:

    • create_agent: 创建一个新的Agent
    • get_agent: 获取一个Agent的详细信息
    • list_agents: 列出所有Agent
    • delete_agent: 删除一个Agent
  2. 消息处理:

    • process_message: 处理一条用户消息,将其发送给对应的Agent,并返回Agent的响应
    • _step: Agent处理消息的核心逻辑,包括添加用户消息、发送给LLM、处理LLM响应、执行函数调用、生成最终回复等步骤
  3. 内存管理:

    • get_agent_memory: 获取Agent的当前内存状态
    • update_agent_memory: 更新Agent的内存
  4. 函数管理:

    • list_functions: 列出所有可用的函数
    • create_function: 添加一个新函数
    • delete_function: 删除一个函数

SyncServer在收到请求后,会调用相应的方法来处理,并返回结果。这些方法大多调用了Agent的相关方法,可以看作是对Agent能力的一层封装。

此外,SyncServer还负责:

  • 管理Agent的生命周期(创建、保存、加载、删除)
  • 协调Agent与其他组件(如MemoryStoreFunctionRegistry等)的交互
  • 处理异常情况,确保服务的稳定性

6. LLM适配

6.1 不同LLM接口的异同

主流的LLM服务在接口设计上存在一些差异,例如:

  • OpenAI使用messages参数传递上下文,而Anthropic使用prompt参数
  • OpenAI使用functions参数描述可用函数,而Anthropic使用插件系统
  • OpenAI使用temperature控制输出的随机性,而Anthropic使用temperaturetop_k的组合

但它们在核心功能上是一致的,都支持:

  • 传入上下文(Context)
  • 流式响应(Streaming)
  • 函数调用(Function Calling)

这为适配不同LLM提供了可能。

6.2 llm_api模块的适配方法

MemGPT在llm_api模块中封装了与不同LLM的交互逻辑。其中,openai.pyanthropic.py等文件分别对应了不同LLM的适配代码。

openai.py为例,其主要方法包括:

  • openai_chat_completions_request: 发送一个Chat Completion请求并返回结果,对应OpenAI的/v1/chat/completions接口
  • openai_chat_completions_stream: 发送一个Chat Completion请求并流式返回结果,对应OpenAI的/v1/chat/completions接口的stream模式
  • openai_embeddings_request: 发送一个Embeddings请求并返回结果,对应OpenAI的/v1/embeddings接口

这些方法接收统一的参数(如api_keymodelmessages等),将其转换为对应LLM的请求格式,发送请求,并将响应转换回统一的格式。

例如,openai_chat_completions_request的核心逻辑如下:

def openai_chat_completions_request(
api_key: str, model: str, messages: List[dict]
) -> dict:
url = f"{OPENAI_API_BASE}/v1/chat/completions"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}
data = {
"model": model,
"messages": messages,
}

response = requests.post(url, headers=headers, json=data)
response_data = response.json()

if response.status_code != 200:
raise OpenAIError(response_data)

return response_data

它将messages等参数按照OpenAI的要求进行封装,发送请求,并检查响应状态。如果请求失败,则抛出一个OpenAIError异常。

通过这种方式,llm_api模块屏蔽了不同LLM接口的差异,为上层提供了一致的访问方式。

6.3 ChatML在Agent中的应用

除了API适配,MemGPT还使用了一种名为ChatML的通用语言来构建Agent的Prompt。ChatML是一种基于XML的标记语言,用于描述聊天机器人的行为。

例如,一个简单的ChatML Prompt可能如下:

<chatbot>
<description>
Assistant is a large language model trained by Anthropic to be helpful, harmless, and honest.
</description>

<memory>
<human_attributes>
The human is a 25-year-old male named John. John is a software engineer who loves hiking and photography.
</human_attributes>
<conversation_summary>
John is planning a hiking trip and has asked the assistant for advice on choosing a destination and packing gear.
</conversation_summary>
</memory>

<message>
<human>What are some good beginner-friendly hiking trails in California?</human>
<assistant>Here are some great beginner-friendly hiking trails in California:

1. Fern Canyon Trail in Prairie Creek Redwoods State Park: This easy 1-mile loop takes you through a lush canyon filled with ferns and waterfalls. It's a great introduction to the beauty of the redwood forests.

2. Bumpass Hell Trail in Lassen Volcanic National Park: This 3-mile round trip hike takes you to a fascinating geothermal area with boiling pools and mud pots. The trail is well-maintained and offers stunning views of the park.

3. Moro Rock Trail in Sequoia National Park: This short but steep 0.5-mile hike leads to the top of a granite dome with panoramic views of the surrounding mountains and valleys. There are stairs and handrails to assist hikers.

4. Inspiration Point Trail in Yosemite National Park: This easy 2.6-mile round trip hike offers classic views of Yosemite Valley, including Half Dome and El Capitan. It's a great option for those who want to experience Yosemite's iconic scenery without a strenuous hike.

Remember to always check trail conditions, bring plenty of water, and wear appropriate footwear and sun protection. Let me know if you need any other hiking advice!</assistant>
</message>
</chatbot>

这个Prompt包含了一个对Assistant的描述,一些关于Human的背景信息,以及一段Human-Assistant的对话历史。

MemGPT使用ChatML构建Prompt的主要优势在于:

  • 统一了不同LLM的Prompt格式,提高了可移植性
  • 使用XML标签来组织Prompt结构,提高了可读性和可维护性
  • 可以方便地嵌入上下文信息,如内存(Memory)、函数调用(FunctionCall)等

在实际应用中,MemGPT会根据需要动态生成ChatML Prompt,例如:

def generate_prompt(human_input: str, agent: Agent) -> str:
memory_attributes = {
"human_attributes": agent.memory.human,
"conversation_summary": agent.memory.summary,
}

messages = [
{"human": message.content}
for message in agent.history if message.role == "human"
] + [{"human": human_input}]

return f"""
<chatbot>
<description>{agent.description}</description>
<memory>{dict_to_xml(memory_attributes)}</memory>
<conversation>{messages_to_xml(messages)}</conversation>
</chatbot>
"""

这个函数从Agent的当前状态(如descriptionmemoryhistory)中提取信息,并将它们格式化为ChatML。最终生成的Prompt会被发送给LLM。

7. 总结与思考

7.1 MemGPT的技术特色与优势

综上所述,MemGPT的一些技术特色和优势包括:

  1. 模块化的Agent架构,核心组件如AgentMemoryFunction等都有清晰的抽象
  2. 成熟的记忆管理机制,使用CoreMemoryRecallMemoryArchivalMemory来存储不同层次的知识
  3. 灵活的函数化交互方式,允许用Python定义Agent的能力,并自动生成LLM可用的Schema
  4. 基于FastAPI的服务化实现,以RESTful API的形式暴露Agent的功能
  5. 统一的LLM适配层(llm_api)和Prompt构建语言(ChatML),提高了跨模型的可移植性

这些特色使得MemGPT成为一个功能完备、可扩展、易用的Agant构建框架。

7.2 架构与代码的改进空间

当然,MemGPT还有一些改进的空间,例如:

  1. Agent类的代码比较复杂,包含了状态管理、消息处理、函数调度等多个功能,可以考虑进一步拆分
  2. Memory模块对不同类型的存储后端(如向量数据库、文档数据库等)支持有限,可以抽象出更通用的接口
  3. llm_api模块目前的适配方式较为简单,没有充分考虑LLM的差异(如对话管理、Prompt优化等),可以设计更灵活的适配机制
  4. 缺乏细粒度的权限控制和审计日志等安全特性,在实际应用中可能会有风险
  5. 部署和监控方面的支持还不够完善,缺少配套的日志、指标、告警等设施

未来,MemGPT可以在这些方面加强,进一步提升其可用性和可靠性。

7.3 对Agent开发的启示

MemGPT为Agent开发提供了一个很好的模板。从它的设计中,我们可以学到以下经验:

  1. Agent = LLM + Memory + Functions: 一个好的Agent,应该具备知识储备(Memory)、推理能力(LLM)和执行能力(Functions)。
  2. 结构化的Memory管理: Agent的记忆应该按照不同的时效性和作用,存储在不同的介质中,并提供统一的访问接口。
  3. 函数即能力: 将Agent的技能抽象为函数,可以使其更容易扩展和组合。LLM负责调用这些函数,而具体的执行逻辑则由Python代码来实现。
  4. 服务化与标准化: 将Agent封装为Web服务,可以使其更容易集成到不同的应用中。同时,使用RESTful等成熟的API设计规范,可以提高互操作性。
  5. 框架与平台的分离: MemGPT将Agent的核心逻辑与具体的LLM实现分离,使得它可以适配不同的LLM平台。这种分层的架构具有很好的灵活性。

总之,MemGPT展示了如何从"Agent即服务"的角度来设计一个对话式AI系统。它的很多思路,如Memory管理、函数抽象、服务封装等,都值得我们在构建自己的Agent时借鉴。

当然,Agent开发还有很多其他的课题,如Few-shot Learning、Reinforcement Learning、安全与伦理等,这些都超出了MemGPT的讨论范畴。一个真正智能的Agent,需要在认知、学习、推理、执行等多个层面进行探索和创新。

MemGPT为这一目标提供了一个很好的起点。希望本文的分析能够帮助读者更好地理解其内部机制,并启发大家去设计和构建更强大的Agent系统。让我们一起努力,让智能对话成为未来人机交互的主流形式。

系统设计学习笔记 - 1

· 阅读需 13 分钟

设计一个支持数百万用户的系统是一个极具挑战性的过程,需要不断的改进和优化。

单服务器架构

千里之行,始于足下,构建复杂系统也是如此。首先,我们从一个简单的单服务器架构开始,所有组件(如 web 应用、数据库、缓存等)都运行在一台服务器上(图 1-1)。

Imgur

请求流程与流量来源

理解这种架构,我们可以从请求流程和流量来源入手。

请求流程(图 1-2):

  1. 用户通过域名(如 api.mysite.com)访问网站,域名系统(DNS)通常由第三方提供,而非自建。
  2. DNS 返回 IP 地址(例如:15.125.23.214)给浏览器或移动应用。
  3. 获取 IP 地址后,发送 HTTP 请求到 Web 服务器。
  4. Web 服务器返回 HTML 页面或 JSON 响应进行渲染。

流量来源:

  • Web 应用: 使用服务器端语言(如 Java、Python 等)处理业务逻辑和存储,客户端语言(如HTML 和 JavaScript)用于展示。
  • 移动应用: 使用 HTTP 协议与 Web 服务器通信,常用 JSON 作为 API 响应格式。

数据库

随着用户基数的增长,一台服务器已无法满足需求,我们需要将 Web / 移动流量和数据库分离成独立的服务器(图 1-3)。这种分离使得 Web 层和数据层可以独立扩展。

选择哪种数据库?

我们可以选择传统关系型数据库或非关系型数据库。

关系型数据库(RDBMS/SQL): 如 MySQL、Oracle、PostgreSQL 等,数据以表格和行的形式存储,可以进行 SQL 跨表查询。

非关系型数据库(NoSQL): 如 CouchDB、Neo4j、Cassandra、HBase、Amazon DynamoDB 等,分为键值存储、图存储、列存储和文档存储四类。通常不支持联表查询。

对于大多数开发者来说,关系型数据库是首选,因为其成熟可靠。但在某些特殊场景下,非关系型数据库可能更合适:

  • 需要超低延迟。
  • 数据非结构化或无关系。
  • 只需序列化和反序列化数据(如 JSON、XML、YAML 等)。
  • 需要存储大量数据。

垂直扩展 vs. 水平扩展

垂直扩展(scale-up): 增加服务器的 CPU、内存等资源。

水平扩展(scale-out): 增加服务器数量。

当流量较小时,垂直扩展是个好选择,因为其简单直接。但它有明显的局限性:

  • 垂直扩展有硬性限制,无法无限增加资源。
  • 无法实现故障转移和冗余,单服务器故障会导致网站完全瘫痪。

对于大规模应用,水平扩展更为理想。为解决直接连接 Web 服务器的弊端,我们引入了负载均衡。

负载均衡

负载均衡器将流量均匀分配到多个 Web 服务器,用户通过负载均衡器的公共IP访问Web服务器(图 1-4)。这种设计提高了系统的可用性和容错性。

负载均衡解决了无故障转移问题,并提升了 Web 层的可用性:

  • 当服务器 1 离线时,流量自动转向服务器 2。
  • 如果流量激增,只需增加更多服务器,负载均衡器自动分配请求。

数据库复制

数据库复制通常采用主从复制模式(图 1-5),主数据库负责写操作,从数据库负责读操作。

数据库复制的优点:

  • 性能提升: 读操作分散到多个从数据库,提高并行处理能力。
  • 可靠性: 数据在多个位置复制,即使发生灾害也不必担心数据丢失。
  • 高可用性: 数据库离线时,可访问其他数据库服务器的数据。

在生产系统中,提升新的主数据库较为复杂,因为从数据库中的数据可能不是最新的,需要通过数据恢复脚本更新缺失的数据。虽然多主复制和环形复制等其他复制方法可以帮助解决这个问题,但它们的设置较为复杂,超出了本书的讨论范围。

缓存层

缓存是一种临时存储,存储常用数据以提高访问速度。独立的缓存层(图 1-7)可减轻数据库负载,并能独立扩展。常用的缓存策略包括读穿缓存等。

缓存系统的考虑因素:

  • 使用时机: 数据频繁读取但不经常修改时使用缓存。重要数据应保存在持久化存储中,因为缓存服务器重启后数据会丢失。
  • 过期策略: 实现数据过期策略,以防止缓存数据永久存储在内存中。过期时间不宜过短或过长,以平衡频繁加载数据和数据陈旧的问题。
  • 一致性: 保持数据存储与缓存的一致性是一个挑战,尤其是在跨区域扩展时。
  • 故障缓解: 单一缓存服务器是潜在的单点故障(SPOF),建议在不同数据中心部署多个缓存服务器,并适当预留内存缓冲。
  • 淘汰策略: 缓存满时,采用LRU(最近最少使用)等策略淘汰旧数据。

内容分发网络(CDN)

CDN 通过地理分布的服务器网络分发静态内容,如图片、视频、CSS、JavaScript 文件等。用户访问网站时,最近的 CDN 服务器提供静态内容,从而加快加载速度(图 1-10)。

使用CDN的考虑因素:

  • 成本: CDN 由第三方提供,按数据传输量收费。缓存不常用的资源无显著益处,应将其移出 CDN。
  • 缓存过期设置: 为时效性内容设置适当的缓存过期时间,避免内容过期或频繁重新加载。
  • CDN 故障应对: 处理 CDN 故障时,客户端应能从源站点请求资源。
  • 文件失效: 通过 API 或对象版本控制失效 CDN 缓存文件。

无状态Web层

为实现水平扩展,我们需要将状态(如用户会话数据)移出Web层,存储在持久化存储中。这使得Web层成为无状态层,易于扩展(图 1-14)。

数据中心

为了提高可用性和用户体验,我们需要支持多个数据中心(图 1-15)。geoDNS 服务根据用户位置将流量分配到最近的数据中心。若某数据中心故障,流量自动重定向到其他数据中心。

实现多数据中心架构需要解决以下技术挑战:

  • 流量重定向: 使用 GeoDNS 将流量导向最近的数据中心。
  • 数据同步: 通过多数据中心复制策略,确保不同区域用户的数据一致性。
  • 测试与部署: 使用自动化部署工具保持各数据中心服务一致性。

消息队列

消息队列支持异步通信,使系统各组件解耦,便于独立扩展和故障恢复(图 1-17)。例如,图 1-18展示了照片处理任务的消息队列架构,生产者将任务发布到队列,消费者从队列中获取任务进行处理。

日志、监控、自动化

随着系统规模的扩大,日志、监控和自动化工具变得必不可少。

日志: 监控错误日志有助于识别系统中的错误和问题。可以在每台服务器上监控错误日志,或使用工具将其汇总到集中服务中,便于搜索和查看。

监控: 收集不同类型的指标有助于获取业务洞察和了解系统健康状况。常用的指标包括:

  • 主机级别指标: CPU、内存、磁盘 I/O 等。
  • 汇总级别指标: 如整个数据库层、缓存层的性能。
  • 关键业务指标: 日活跃用户、留存率、收入等。

自动化: 随着系统规模和复杂度的增加,构建或利用自动化工具提高生产力至关重要。持续集成是一个好实践,每次代码提交通过自动化验证,帮助团队早期发现问题。自动化构建、测试、部署流程也能显著提高开发者的生产力。

添加消息队列和工具

图 1-19展示了更新后的设计。由于空间限制,仅展示了一个数据中心。设计中包括消息队列、日志、监控、度量和自动化工具,使系统更加松耦合和容错。

数据库扩展

数据库扩展主要有垂直扩展和水平扩展两种方式。

垂直扩展: 增加数据库服务器的资源,如 CPU、内存等。但有硬性限制和单点故障风险。

水平扩展(分片): 将大数据库分为多个小的部分(分片)。选择合适的分片键(如用户 ID)是关键,确保数据均匀分布(图 1-22)。

在图 1-23中,我们对数据库进行了分片,以支持快速增长的数据流量,同时将一些非关系型功能迁移到NoSQL数据存储中,以减轻数据库负载。

结论

扩展系统是一个迭代过程,需要不断优化和引入新策略。以下是支持数百万用户的系统设计要点:

  • Web 层无状态化
  • 各层冗余设计
  • 尽量缓存数据
  • 支持多数据中心
  • 静态资源托管在 CDN
  • 数据层通过分片扩展
  • 各层独立服务化
  • 监控系统并使用自动化工具

https SSL 证书配置及 nginx 部署方式

· 阅读需 7 分钟

原理

HTTPS

HTTPS(Hypertext Transfer Protocol Secure)是基于传输层安全协议(TLS)的HTTP协议的安全版本。它通过使用 SSL/TLS 协议对HTTP的通信进行加密,确保数据在传输过程中的安全性和完整性。与 HTTP 相比,HTTPS 提供了更高的安全性,可以有效地防止中间人攻击、窃听、数据篡改和其他安全威胁。

SSL/TLS

SSL(Secure Sockets Layer)和TLS(Transport Layer Security)是加密协议,用于确保通信安全并保护数据在互联网上的传输。SSL 是 TLS 的前身,随着时间的推移,TLS 逐渐取代了 SSL 成为更安全和可靠的加密协议。它们使用了公钥加密和对称加密技术来建立安全的通信通道,并确保数据在传输过程中不会被窃听或篡改。

证书

在 SSL/TLS 通信中,证书用于验证通信双方的身份,并确保数据的安全性。SSL证书包含了服务器的公钥和一些基本信息,由证书颁发机构(CA)签名,用于确认服务器身份的真实性。客户端可以使用这些证书验证服务器的身份,并确保通信的安全性。证书通常包含服务器的域名、组织信息、签发机构信息以及有效期等。

SSL 证书获取方式

localhost 证书

要为 localhost 生成 SSL 证书,可以使用 Certbot 的自签名证书功能。但是需要注意的是,Certbot 通常用于在公共域名上生成有效的 SSL 证书。如果你只是想在本地开发环境中使用 SSL,你可以考虑使用自签名证书。以下是一个使用 OpenSSL 生成自签名证书的示例:

  1. 首先,确保你的系统上已经安装了 OpenSSL。

  2. 打开终端并导航到你希望保存证书的目录。

  3. 运行以下命令以生成私钥:

openssl genrsa -out localhost.key 2048
  1. 然后,使用以下命令生成证书签名请求(CSR):
openssl req -new -key localhost.key -out localhost.csr

在这个过程中,你会被要求提供一些信息,包括国家、地区、组织名称等。你可以根据需要自行填写。

  1. 最后,使用以下命令生成自签名证书:
openssl x509 -req -days 365 -in localhost.csr -signkey localhost.key -out localhost.crt

这将生成一个有效期为 365 天的自签名证书。

完成这些步骤后,你可以将生成的 localhost.crtlocalhost.key 文件用作你的 localhost SSL 证书。

请注意,由于这是一个自签名证书,你的浏览器可能会警告你该证书不受信任。在生产环境中,你应该使用受信任的证书颁发机构(CA)签名的证书。

Let's Encrypt 介绍

Let's Encrypt是一个由非营利组织提供的免费、自动化的证书颁发服务,旨在使网站加密变得简单易行。它提供了简单的工具和 API,使网站管理员能够轻松地获取免费的 SSL 证书,从而为其网站启用 HTTPS 连接。Let's Encrypt证书的有效期为90天,但可以通过自动续订进行更新。

下面是在 docker 运行 nginx 支持 https 的方式:

  1. 安装 certbot 工具: 首先,你需要在你的服务器上安装 certbot 工具,它是与 Let's Encrypt 一起使用的官方工具。你可以通过以下命令来安装:

    sudo apt-get update
    sudo apt-get install certbot
  2. 上 cloudflare 或者你托管域的网站上更新 DNS 设置。

  3. 获取证书: 一旦安装了 certbot,你可以运行以下命令来获取证书:

    sudo certbot certonly --standalone -d <domain>

    这将启动一个临时的 web 服务器,用于与 Let's Encrypt 进行验证并获取证书。

  4. 证书文件位置: 证书和私钥文件将会被存储在 certbot 默认的路径中,你可以根据你的需求将其拷贝到适当的位置。在一般情况下,证书文件通常会位于 /etc/letsencrypt/live/<domain>/

  5. 配置 Nginx: 你需要修改你的 nginx.conf 文件以使用 SSL 证书,并将所有的 HTTP 请求重定向到 HTTPS。你需要添加类似下面的配置:

    server {
    listen 443 ssl;
    server_name <domain>;

    ssl_certificate /etc/letsencrypt/live/<domain>fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<domain>/privkey.pem;

    location / {
    proxy_pass <proxy_pass>;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    }
    }

    请确保 Nginx 可以访问证书文件的路径。

  6. 启动 Nginx 容器: 最后,你需要启动 Nginx 容器以使新的配置生效:

    sudo docker run --name mynginx -v /etc/letsencrypt:/etc/letsencrypt -v ./nginx.conf:/etc/nginx/nginx.conf:ro -p 80:80 -p 443:443 -d --network=host nginx

这些步骤将帮助你在使用 Docker 运行的 Nginx 服务器上配置 Let's Encrypt 的 SSL 证书。通过这些步骤,你应该能够启用 HTTPS 并保护你的网站的安全性。

Windows 系统中 Powershell 按键设置

· 阅读需 1 分钟

在使用 Powershell 时,有时想绑定一些自定义的快捷键,可以使用如下两条命令:

Set-PSReadlineKeyHandler -Key UpArrow -Function HistorySearchBackward
Set-PSReadlineKeyHandler -Key DownArrow -Function HistorySearchForward

注,powershell profile 的位置可以使用如下命令查看:

$PROFILE | Select-Object *

详见 https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles?view=powershell-7.3

Ubuntu 防火墙设置

· 阅读需 3 分钟

UFW(Uncomplicated Firewall)是一个用户友好的前端界面,用于管理基于netfilter的iptables防火墙。以下是使用UFW的一些常见方式:

  1. 启用和禁用UFW:在开始配置之前,您需要启用UFW。这可以通过以下命令完成:

    启用: sudo ufw enable

    禁用: sudo ufw disable

  2. 查看UFW状态:要查看UFW的当前状态和规则列表,可以使用以下命令:

    sudo ufw status verbose

  3. 添加规则:您可以通过指定端口号和协议(如TCP或UDP)来添加规则。例如,如果您要允许所有传入的SSH连接,可以使用以下命令:

    sudo ufw allow 22/tcp

    或者,您也可以使用服务名称,如:

    sudo ufw allow ssh

  4. 删除规则:如果您需要删除规则,可以使用 delete 参数。例如,要删除上面添加的规则,可以使用以下命令:

    sudo ufw delete allow 22/tcp

  5. 允许/阻止特定IP地址:您可以允许或阻止来自特定IP地址的连接。例如,以下命令允许来自192.168.1.1的所有连接:

    sudo ufw allow from 192.168.1.1

    而以下命令阻止来自192.168.1.1的所有连接:

    sudo ufw deny from 192.168.1.1

  6. 设置默认策略:UFW允许您设置默认的阻止或允许策略。例如,您可以设置默认拒绝所有传入的连接,然后只允许需要的服务。以下命令设置默认策略为拒绝所有传入的连接:

    sudo ufw default deny incoming

    并允许所有传出的连接:

    sudo ufw default allow outgoing

这只是UFW的一些基本用法。UFW还提供了更高级的功能,如规则排序、日志管理等。

Docker 配置方式

· 阅读需 3 分钟

在这篇文章中,我将指导你如何在你的系统上配置 Docker。我们将分别介绍 Linux 和 MacOS 下的安装方式。

Linux 系统下的安装步骤

1. 卸载旧版本 Docker

首先,我们需要确保系统中没有安装旧版本的 Docker。你可以通过以下命令进行卸载:

sudo apt-get remove docker docker-engine docker.io containerd runc

2. 更新 apt 包索引并安装依赖

接下来,让我们更新系统的 apt 包索引,并安装 Docker 所需的依赖:

sudo apt-get update
sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common

3. 添加 Docker 的官方 GPG 密钥

在安装 Docker 之前,我们需要添加 Docker 的官方 GPG 密钥。这是一个安全步骤,用于验证下载的包是否是原始的:

curl -fsSL https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/gpg | sudo apt-key add -

接下来,我们验证是否已经成功添加了带有指纹的密钥:

sudo apt-key fingerprint 0EBFCD88

4. 设置稳定版仓库

现在,我们需要设置 Docker 的稳定版仓库,以便从中下载和安装 Docker:

sudo add-apt-repository \
"deb [arch=amd64] https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/ \
$(lsb_release -cs) \
stable"

5. 安装最新版本的 Docker Engine-Community 和 containerd

一切准备就绪后,我们可以安装最新版本的 Docker Engine-Community 和 containerd 了:

sudo apt-get install docker-ce docker-ce-cli containerd.io

6. 测试 Docker 是否安装成功

安装完成后,我们可以通过以下命令来测试 Docker 是否已经成功安装:

sudo docker info

7. 解决问题

如果在测试过程中遇到问题,你可以通过以下命令来查看问题出在哪里:

sudo dockerd --debug

如果问题与 iptable 相关,你可以参考这个 Github issue。在运行相关指令时,请确保 net.ipv4.ip_forward=1

MacOS 系统下的安装步骤

在 MacOS 上安装 Docker 过程更为简单:

1. 通过 Homebrew 安装 Docker

首先,我们通过 Homebrew 来安装 Docker:

brew install --cask docker

2. 启动 Docker

安装完成后,你可以在应用中找到 Docker 图标并点击运行。

配置 Docker 代理

无论你是在哪种操作系统下安装 Docker,如果你需要配置 Docker 代理,你可以参考这篇文章

Enable Colored Logs in Supervisord with Unbuffer

· 阅读需 2 分钟

When using Supervisord to manage your application's processes, you may encounter issues with colored logs not being correctly displayed in the stderr_logfile. One possible solution is to use the unbuffer command, which disables buffering and allows color escape sequences to be printed to the log files.

To use unbuffer with Supervisord, modify your supervisord.conf file to include the following:

[program:my_program]
...
command=unbuffer python my_program.py
stdout_logfile=/var/log/my_program.log
stderr_logfile=/var/log/my_program_err.log

This will start your program through the unbuffer command, which will disable buffering and allow color escape sequences to be printed to the stderr_logfile.

Please note that unbuffer is part of the expect package, so you may need to install it on your system if it's not already available. On Debian-based systems (including Ubuntu), you can install expect by running the following command:

sudo apt-get install expect

Enabling Color Output with watch

If you're using the watch command to monitor your log files, you may notice that the color disappears when using the command. This is because watch does not recognize color escape sequences by default.

To enable color output while using the watch command, you can use the -c or --color option. For example:

watch -n 1 --color "tail -n xx /var/log/xxx.log"

This will tell watch to enable color output and pass the colored output to the terminal.

We hope this solution helps you enable colored logs in your Supervisord-managed applications!


使用 Supervisord 管理应用程序时,彩色日志可能未能正确显示在 stderr_logfile 中。使用 unbuffer 命令可以解决这个问题,具体操作如下:

  • 修改 supervisord.conf 文件,添加以下内容:

    [program:my_program]
    ...
    command=unbuffer python my_program.py
    stdout_logfile=/var/log/my_program.log
    stderr_logfile=/var/log/my_program_err.log
  • 如果系统上没有安装 expect 软件包,需要先安装,可以使用以下命令:

    sudo apt-get install expect

如果你使用 watch 命令来监视日志文件,可以使用 -c--color 选项启用彩色输出:

watch -n 1 --color "tail -n xx /var/log/xxx.log"

希望这篇文章可以帮助你解决彩色日志的问题!

FastAPI 服务提供 OpenAI 流式接口

· 阅读需 1 分钟

demo 代码如下:

import requests
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse

app = FastAPI()


async def request_stream():
header = {
"Content-Type": "application/json; charset=utf-8",
"Authorization": "Bearer sk-xxx", # OpenAI API Key
}

session = requests.Session()
resp = session.post(
"https://api.openai.com/v1/completions",
json={
"model": "text-davinci-003",
"prompt": "Count to 100, with a comma between each number and no newlines. 1, 2, 3, ",
"max_tokens": 100,
"temperature": 0,
"stream": True,
},
headers=header,
stream=True,
)
for line in resp.iter_lines():
print(f"{line=}")
if line:
yield f"{line.decode('utf-8')}\n\n"


@app.post("/stream")
async def stream(request: Request):
async def event_generator():
async for message in request_stream():
if await request.is_disconnected():
break
yield message

return StreamingResponse(event_generator(), media_type="text/event-stream")


@app.get("/")
async def index():
return "Hello World"


# if __name__ == '__main__':
# for chunk in request_stream():
# try:
# chunk = json.loads(chunk.replace("data: ", ""))
# print(chunk["choices"][0]["text"], end="", flush=True)
# except:
# continue

dig - DNS 查询工具

· 阅读需 3 分钟

dig 是一种强大的 DNS 查询工具,它可以在命令行中使用。以下是几个使用例:

查询A记录

假设我们想要查询 "example.com" 的 A 记录,可以在命令行中输入以下命令:

dig example.com A

这将返回"example.com"的IP地址。

查询MX记录

假设我们想要查询 "example.com" 的 MX 记录(邮件服务器记录),可以在命令行中输入以下命令:

dig example.com MX

这将返回"example.com"的 MX 记录信息,包括邮件服务器的名称和优先级。

查询DNS服务器详细信息

假设我们想要查询 "example.com" 域名服务器的 IP 地址,可以在命令行中输入以下命令:

dig example.com NS

这将返回 "example.com" 的域名服务器信息,包括名称和IP地址。

使用选项和标志

假设我们想要查询 "example.com" 的 IP 地址,并仅返回 IP 地址,可以在命令行中输入以下命令:

dig example.com A +short

这将仅返回 "example.com" 的IP地址,而不包括其他信息。

自动化脚本

假设我们需要查找多个域名的DNS信息,可以编写一个自动化脚本来执行查询。以下是一个示例脚本:

#!/bin/bash
for domain in example.com google.com facebook.com
do
echo "Domain: $domain"
dig $domain A +short
echo ""
done

这将自动查询三个域名的 A 记录,并输出结果。

总的来说,dig 是一个非常有用的 DNS 查询工具,它可以帮助用户快速地查找 DNS 记录和服务器信息。无论是在命令行中使用还是通过脚本自动化,dig 都是一个非常强大和灵活的工具。

ssh 使用代理的方式

· 阅读需 2 分钟

ssh xx.xx.xx.xx -o "ProxyCommand=nc -X connect -x <proxy_ip>:<proxy_port> %h %p"

解释:

这是一个使用 SSH 协议连接到远程主机的命令,同时使用代理服务器进行连接的选项。

具体地说,这个命令中的 xx.xx.xx.xx 表示目标远程主机的 IP 地址或域名。-o 参数后面跟着的是一个选项,这里使用了 ProxyCommand 选项,该选项指定了一个代理命令来建立 SSH 连接。在这个例子中,代理命令是 nc,用于将 SSH 流量通过代理服务器转发到目标远程主机。

  • X connect -x <proxy_ip>:<proxy_port>nc 命令中的选项,X connect 表示使用 CONNECT 方法与代理服务器进行连接,x <proxy_ip>:<proxy_port> 指定了代理服务器的主机名与端口号。

%h%pnc 命令中的占位符,分别表示目标远程主机的主机名或 IP 地址和端口号。