From 14289c2d8f9c2853739636161ecb3e317bf4c99e Mon Sep 17 00:00:00 2001 From: renee <50965960+wurenee@users.noreply.github.com> Date: Fri, 30 Jan 2026 22:49:12 -0800 Subject: [PATCH] =?UTF-8?q?=E8=81=94=E5=85=A5SQLite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/ai/ai.py | 145 ++++ app/ai/ai_with_example_output.ipynb | 1149 +++++++++++++++++++++++++++ app/ai/graph.py | 33 - app/ai/nodes.py | 78 -- app/ai/services/memory_service.py | 25 - app/ai/services/summary_service.py | 65 -- app/ai/state.py | 12 - 7 files changed, 1294 insertions(+), 213 deletions(-) create mode 100644 app/ai/ai.py create mode 100644 app/ai/ai_with_example_output.ipynb delete mode 100644 app/ai/graph.py delete mode 100644 app/ai/nodes.py delete mode 100644 app/ai/services/memory_service.py delete mode 100644 app/ai/services/summary_service.py delete mode 100644 app/ai/state.py diff --git a/app/ai/ai.py b/app/ai/ai.py new file mode 100644 index 0000000..30cb198 --- /dev/null +++ b/app/ai/ai.py @@ -0,0 +1,145 @@ + +# pip install -q langgraph-checkpoint-sqlite + +import sqlite3 +from google.colab import userdata +from typing import Literal, TypedDict, Annotated +from langchain_google_genai import ChatGoogleGenerativeAI +from langchain_core.messages import SystemMessage, HumanMessage, RemoveMessage, AnyMessage +from langgraph.graph import StateGraph, START, END +from langgraph.checkpoint.sqlite import SqliteSaver +from langgraph.graph.message import add_messages +from langchain_core.runnables import RunnableConfig + +# --- 1. 状态定义 --- +class State(TypedDict): + messages: Annotated[list[AnyMessage], add_messages] + summary: str # 永久存储在数据库中的摘要内容 + +# --- 2. 核心逻辑 --- +llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature = 0.7, google_api_key='AIzaSyCfkiIgq4FmH5siBp3Iw6MRCml5zeSURnY') + +def call_model(state: State, config: RunnableConfig): + """对话节点:融合了动态 Prompt 和 长期摘要""" + + # 获取当前 session 特有的 System Prompt(如果没传则使用默认) + configurable = config.get("configurable", {}) + system_base_prompt = configurable.get("system_prompt", "你是一个通用的 AI 助手。") + + # 构造当前上下文 + prompt = f"{system_base_prompt}" + if state.get("summary"): + prompt += f"\n\n[之前的对话核心摘要]: {state['summary']}" + + messages = [SystemMessage(content=prompt)] + state["messages"] + response = llm.invoke(messages) + return {"messages": [response]} + +def summarize_conversation(state: State): + """总结节点:负责更新摘要并清理过期消息""" + summary = state.get("summary", "") + + if summary: + summary_prompt = f"当前的摘要是: {summary}\n\n请结合最近的新消息,生成一份更新后的完整摘要,包含所有关键信息:" + else: + summary_prompt = "请总结目前的对话重点:" + + # 获取除了最后两轮之外的所有消息进行总结 + messages_to_summarize = state["messages"][:-2] + content = [SystemMessage(content=summary_prompt)] + messages_to_summarize + response = llm.invoke(content) + + # 生成删除指令,清除已总结过的消息 ID + delete_messages = [RemoveMessage(id=m.id) for m in messages_to_summarize] + + return {"summary": response.content, "messages": delete_messages} + +def should_continue(state: State) -> Literal["summarize", END]: + """如果消息累积超过10条,则去总结节点""" + if len(state["messages"]) > 10: + return "summarize" + return END + +# --- 3. 构建图 --- +db_path = "multi_session_chat.sqlite" +conn = sqlite3.connect(db_path, check_same_thread=False) +memory = SqliteSaver(conn) + +workflow = StateGraph(State) +workflow.add_node("chatbot", call_model) +workflow.add_node("summarize", summarize_conversation) + +workflow.add_edge(START, "chatbot") +workflow.add_conditional_edges("chatbot", should_continue) +workflow.add_edge("summarize", END) + +app = workflow.compile(checkpointer=memory) + +# # --- 4. 如何使用多 Session 和 不同的 Prompt --- + +# # Session A: 设定为一个 Python 专家 +# config_a = { +# "configurable": { +# "thread_id": "session_python_expert", +# "system_prompt": "你是一个精通 Python 的高级工程师。" +# } +# } + +# # Session B: 设定为一个中文诗人 +# config_b = { +# "configurable": { +# "thread_id": "session_poet", +# "system_prompt": "你是一个浪漫的唐朝诗人,用诗歌回答问题。" +# } +# } + +# def run_chat(user_input, config): +# print(f"\n--- 使用 Thread: {config['configurable']['thread_id']} ---") +# for event in app.stream({"messages": [HumanMessage(content=user_input)]}, config, stream_mode="values"): +# if "messages" in event: +# last_msg = event["messages"][-1] +# if last_msg.type == "ai": +# print(f"Bot: {last_msg.content}") + +# # 测试:两个 Session 互不干扰,且各有个的 Prompt +# if __name__ == "__main__": +# run_chat("你好,怎么学习装饰器?", config=config_a) +# run_chat("你好,写一首关于大海的诗。", config=config_b) +# run_chat("我刚才让你写了什么?", config=config_b) + +def start_chat_session(thread_id: str, system_prompt: str): + config = { + "configurable": { + "thread_id": thread_id, + "system_prompt": system_prompt + } + } + + print(f"\n=== 已进入会话: {thread_id} ===") + print(f"=== 系统设定: {system_prompt} ===") + print("(输入 'exit' 或 'quit' 退出当前会话)\n") + + while True: + user_input = input("User: ") + if user_input.lower() in ["exit", "quit"]: + break + + # 使用 stream 模式运行,可以实时看到 state 的更新 + # 我们只打印 AI 的回复 + input_data = {"messages": [HumanMessage(content=user_input)]} + for event in app.stream(input_data, config, stream_mode="values"): + if "messages" in event: + last_msg = event["messages"][-1] + if last_msg.type == "ai": + print(f"Bot: {last_msg.content}") + +if __name__ == "__main__": + # 模拟场景 1: Python 专家会话 + # 即使你关掉程序再运行,只要 thread_id 还是 'py_expert_001',记忆就会从 sqlite 读取 + start_chat_session( + thread_id="py_expert_001", + system_prompt="你是一个精通 Python 的架构师。" + ) + +# from IPython.display import Image, display +# display(Image(app.get_graph().draw_mermaid_png())) \ No newline at end of file diff --git a/app/ai/ai_with_example_output.ipynb b/app/ai/ai_with_example_output.ipynb new file mode 100644 index 0000000..5cc7aa0 --- /dev/null +++ b/app/ai/ai_with_example_output.ipynb @@ -0,0 +1,1149 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "code", + "source": [ + "!pip install -q langgraph-checkpoint-sqlite" + ], + "metadata": { + "id": "xx7d1mBroYgg" + }, + "execution_count": 15, + "outputs": [] + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "id": "nfLqX-gWo_VF" + }, + "execution_count": 16, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "import sqlite3\n", + "from google.colab import userdata\n", + "from typing import Literal, TypedDict, Annotated\n", + "from langchain_google_genai import ChatGoogleGenerativeAI\n", + "from langchain_core.messages import SystemMessage, HumanMessage, RemoveMessage, AnyMessage\n", + "from langgraph.graph import StateGraph, START, END\n", + "from langgraph.checkpoint.sqlite import SqliteSaver\n", + "from langgraph.graph.message import add_messages\n", + "from langchain_core.runnables import RunnableConfig\n", + "\n", + "# --- 1. 状态定义 ---\n", + "class State(TypedDict):\n", + " messages: Annotated[list[AnyMessage], add_messages]\n", + " summary: str # 永久存储在数据库中的摘要内容\n", + "\n", + "# --- 2. 核心逻辑 ---\n", + "llm = ChatGoogleGenerativeAI(model=\"gemini-2.5-flash\", temperature = 0.7, google_api_key='AIzaSyCfkiIgq4FmH5siBp3Iw6MRCml5zeSURnY')\n", + "\n", + "def call_model(state: State, config: RunnableConfig):\n", + " \"\"\"对话节点:融合了动态 Prompt 和 长期摘要\"\"\"\n", + "\n", + " # 获取当前 session 特有的 System Prompt(如果没传则使用默认)\n", + " configurable = config.get(\"configurable\", {})\n", + " system_base_prompt = configurable.get(\"system_prompt\", \"你是一个通用的 AI 助手。\")\n", + "\n", + " # 构造当前上下文\n", + " prompt = f\"{system_base_prompt}\"\n", + " if state.get(\"summary\"):\n", + " prompt += f\"\\n\\n[之前的对话核心摘要]: {state['summary']}\"\n", + "\n", + " messages = [SystemMessage(content=prompt)] + state[\"messages\"]\n", + " response = llm.invoke(messages)\n", + " return {\"messages\": [response]}\n", + "\n", + "def summarize_conversation(state: State):\n", + " \"\"\"总结节点:负责更新摘要并清理过期消息\"\"\"\n", + " summary = state.get(\"summary\", \"\")\n", + "\n", + " if summary:\n", + " summary_prompt = f\"当前的摘要是: {summary}\\n\\n请结合最近的新消息,生成一份更新后的完整摘要,包含所有关键信息:\"\n", + " else:\n", + " summary_prompt = \"请总结目前的对话重点:\"\n", + "\n", + " # 获取除了最后两轮之外的所有消息进行总结\n", + " messages_to_summarize = state[\"messages\"][:-2]\n", + " content = [SystemMessage(content=summary_prompt)] + messages_to_summarize\n", + " response = llm.invoke(content)\n", + "\n", + " # 生成删除指令,清除已总结过的消息 ID\n", + " delete_messages = [RemoveMessage(id=m.id) for m in messages_to_summarize]\n", + "\n", + " return {\"summary\": response.content, \"messages\": delete_messages}\n", + "\n", + "def should_continue(state: State) -> Literal[\"summarize\", END]:\n", + " \"\"\"如果消息累积超过10条,则去总结节点\"\"\"\n", + " if len(state[\"messages\"]) > 10:\n", + " return \"summarize\"\n", + " return END\n", + "\n", + "# --- 3. 构建图 ---\n", + "db_path = \"multi_session_chat.sqlite\"\n", + "conn = sqlite3.connect(db_path, check_same_thread=False)\n", + "memory = SqliteSaver(conn)\n", + "\n", + "workflow = StateGraph(State)\n", + "workflow.add_node(\"chatbot\", call_model)\n", + "workflow.add_node(\"summarize\", summarize_conversation)\n", + "\n", + "workflow.add_edge(START, \"chatbot\")\n", + "workflow.add_conditional_edges(\"chatbot\", should_continue)\n", + "workflow.add_edge(\"summarize\", END)\n", + "\n", + "app = workflow.compile(checkpointer=memory)\n", + "\n", + "# # --- 4. 如何使用多 Session 和 不同的 Prompt ---\n", + "\n", + "# # Session A: 设定为一个 Python 专家\n", + "# config_a = {\n", + "# \"configurable\": {\n", + "# \"thread_id\": \"session_python_expert\",\n", + "# \"system_prompt\": \"你是一个精通 Python 的高级工程师。\"\n", + "# }\n", + "# }\n", + "\n", + "# # Session B: 设定为一个中文诗人\n", + "# config_b = {\n", + "# \"configurable\": {\n", + "# \"thread_id\": \"session_poet\",\n", + "# \"system_prompt\": \"你是一个浪漫的唐朝诗人,用诗歌回答问题。\"\n", + "# }\n", + "# }\n", + "\n", + "# def run_chat(user_input, config):\n", + "# print(f\"\\n--- 使用 Thread: {config['configurable']['thread_id']} ---\")\n", + "# for event in app.stream({\"messages\": [HumanMessage(content=user_input)]}, config, stream_mode=\"values\"):\n", + "# if \"messages\" in event:\n", + "# last_msg = event[\"messages\"][-1]\n", + "# if last_msg.type == \"ai\":\n", + "# print(f\"Bot: {last_msg.content}\")\n", + "\n", + "# # 测试:两个 Session 互不干扰,且各有个的 Prompt\n", + "# if __name__ == \"__main__\":\n", + "# run_chat(\"你好,怎么学习装饰器?\", config=config_a)\n", + "# run_chat(\"你好,写一首关于大海的诗。\", config=config_b)\n", + "# run_chat(\"我刚才让你写了什么?\", config=config_b)" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "CrvsvAasszAH", + "outputId": "74a671d9-6d45-4d47-c442-ea921d8467e6" + }, + "execution_count": 19, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\n", + "--- 使用 Thread: session_python_expert ---\n", + "Bot: 你好!很高兴你对 Python 装饰器如此感兴趣,这是一个非常强大且优雅的特性。由于你重复了三次,我将为你提供一个**非常详细和结构化的学习路径**,确保你能彻底理解并掌握它。\n", + "\n", + "装饰器是 Python 中一个相对高级但又非常实用的概念。理解它需要你先掌握一些基础知识。\n", + "\n", + "---\n", + "\n", + "### 学习装饰器的路线图\n", + "\n", + "我们将分阶段学习,从最基础的概念开始,逐步深入。\n", + "\n", + "#### 阶段 1:前置知识(基石)\n", + "\n", + "在理解装饰器之前,你需要先掌握以下 Python 概念:\n", + "\n", + "1. **函数是第一类对象 (First-Class Objects)**\n", + " * 这意味着函数可以像普通变量一样被处理:\n", + " * 可以赋值给变量。\n", + " * 可以作为参数传递给其他函数。\n", + " * 可以作为另一个函数的返回值。\n", + " * **为什么重要?** 装饰器就是通过将函数作为参数和返回值来实现的。\n", + "\n", + " ```python\n", + " # 示例 1.1: 函数赋值给变量\n", + " def say_hello(name):\n", + " return f\"Hello, {name}!\"\n", + "\n", + " greet = say_hello # greet 现在也指向 say_hello 函数\n", + " print(greet(\"Alice\")) # 输出: Hello, Alice!\n", + "\n", + " # 示例 1.2: 函数作为参数传递\n", + " def execute_function(func, arg):\n", + " return func(arg)\n", + "\n", + " print(execute_function(say_hello, \"Bob\")) # 输出: Hello, Bob!\n", + "\n", + " # 示例 1.3: 函数作为返回值\n", + " def create_greeter(greeting_word):\n", + " def greeter(name): # 这是一个内部函数\n", + " return f\"{greeting_word}, {name}!\"\n", + " return greeter\n", + "\n", + " morning_greeter = create_greeter(\"Good morning\")\n", + " evening_greeter = create_greeter(\"Good evening\")\n", + "\n", + " print(morning_greeter(\"Charlie\")) # 输出: Good morning, Charlie!\n", + " print(evening_greeter(\"David\")) # 输出: Good evening, David!\n", + " ```\n", + "\n", + "2. **嵌套函数 (Nested Functions)**\n", + " * 在一个函数内部定义另一个函数。\n", + " * **为什么重要?** 装饰器通常会返回一个内部定义的函数。\n", + "\n", + " ```python\n", + " def outer_function(msg):\n", + " def inner_function(): # 这是一个嵌套函数\n", + " print(msg)\n", + " inner_function() # 外部函数调用内部函数\n", + "\n", + " outer_function(\"Hello from inner!\")\n", + " ```\n", + "\n", + "3. **闭包 (Closures)**\n", + " * 闭包是一个函数,它记住了自己被创建时的环境(即它能够访问其外部作用域中的变量,即使外部函数已经执行完毕并返回了)。\n", + " * 这是理解装饰器**最最关键**的概念。\n", + " * **为什么重要?** 装饰器返回的内部函数(通常称为 `wrapper`)就是一个闭包,它能够访问到被装饰的函数(在外部作用域中)。\n", + "\n", + " ```python\n", + " def outer_function_for_closure(msg):\n", + " def inner_function_closure(): # 这是一个闭包\n", + " print(msg) # inner_function_closure 记住了 msg 的值\n", + " return inner_function_closure # 返回内部函数,但它仍然能访问 msg\n", + "\n", + " my_closure = outer_function_for_closure(\"I am a closure!\")\n", + " my_closure() # 即使 outer_function_for_closure 已经执行完毕,my_closure 仍然能打印 msg\n", + " ```\n", + "\n", + "4. **`*args` 和 `**kwargs`**\n", + " * 用于处理不确定数量的位置参数和关键字参数。\n", + " * **为什么重要?** 装饰器通常需要能够装饰任何签名的函数,因此需要用 `*args` 和 `**kwargs` 来传递参数。\n", + "\n", + " ```python\n", + " def my_sum(*args): # 接收任意数量的位置参数\n", + " return sum(args)\n", + "\n", + " def my_printer(**kwargs): # 接收任意数量的关键字参数\n", + " for key, value in kwargs.items():\n", + " print(f\"{key}: {value}\")\n", + "\n", + " print(my_sum(1, 2, 3, 4)) # 输出: 10\n", + " my_printer(name=\"Alice\", age=30) # 输出: name: Alice, age: 30\n", + " ```\n", + "\n", + "---\n", + "\n", + "#### 阶段 2:理解装饰器核心原理\n", + "\n", + "现在,我们把这些概念组合起来。\n", + "\n", + "1. **手动实现“装饰”**\n", + " * 一个装饰器本质上是一个函数,它接收一个函数作为参数,并返回一个新的函数(通常是内部定义的 `wrapper` 函数)。这个 `wrapper` 函数会包含对原函数的调用,并在其前后添加额外的逻辑。\n", + "\n", + " ```python\n", + " # 我们的目标是给任何函数添加一个“问候”功能,在函数执行前打印一条消息\n", + "\n", + " def my_decorator(func): # 装饰器函数,接收一个函数作为参数\n", + " def wrapper(*args, **kwargs): # 内部函数,它将替代原函数被调用\n", + " print(\"--- 准备执行函数 ---\") # 额外逻辑\n", + " result = func(*args, **kwargs) # 调用原函数\n", + " print(\"--- 函数执行完毕 ---\") # 额外逻辑\n", + " return result # 返回原函数的执行结果\n", + " return wrapper # 装饰器返回这个包装器函数\n", + "\n", + " def greet_user(name):\n", + " print(f\"Hello, {name}!\")\n", + " return f\"Greeting for {name}\"\n", + "\n", + " # 手动应用装饰器\n", + " greet_user_decorated = my_decorator(greet_user) # 将 greet_user 传递给装饰器,得到一个新的函数\n", + " returned_value = greet_user_decorated(\"Bob\")\n", + " print(f\"Returned value: {returned_value}\")\n", + "\n", + " # 输出:\n", + " # --- 准备执行函数 ---\n", + " # Hello, Bob!\n", + " # --- 函数执行完毕 ---\n", + " # Returned value: Greeting for Bob\n", + " ```\n", + " * **关键点:** `my_decorator(greet_user)` 返回了一个新的函数 `wrapper`。现在,当你调用 `greet_user_decorated(\"Bob\")` 时,实际上是在调用 `wrapper(\"Bob\")`。`wrapper` 内部会先打印消息,然后调用原始的 `greet_user(\"Bob\")`,再打印另一条消息,最后返回结果。\n", + "\n", + "2. **`@` 语法糖 (Syntactic Sugar)**\n", + " * Python 提供了 `@` 符号,它仅仅是上面手动赋值的简写。\n", + " * `@decorator` 放在函数定义上方,等同于 `my_function = decorator(my_function)`。\n", + "\n", + " ```python\n", + " def my_decorator(func):\n", + " def wrapper(*args, **kwargs):\n", + " print(\"--- 准备执行函数 ---\")\n", + " result = func(*args, **kwargs)\n", + " print(\"--- 函数执行完毕 ---\")\n", + " return result\n", + " return wrapper\n", + "\n", + " @my_decorator # 这行代码等同于 greet_user = my_decorator(greet_user)\n", + " def greet_user(name):\n", + " print(f\"Hello, {name}!\")\n", + " return f\"Greeting for {name}\"\n", + "\n", + " returned_value = greet_user(\"Alice\") # 现在直接调用 greet_user 就会执行装饰过的逻辑\n", + " print(f\"Returned value: {returned_value}\")\n", + "\n", + " # 输出与手动应用时相同\n", + " ```\n", + " * **理解 `@` 的魔力:** 当 Python 解释器看到 `@my_decorator` 时,它会做两件事:\n", + " 1. 定义 `greet_user` 函数。\n", + " 2. 将 `greet_user` 函数作为参数传递给 `my_decorator`。\n", + " 3. 将 `my_decorator` 返回的新函数(即 `wrapper`)重新赋值给 `greet_user`。\n", + " 所以,当你后续代码中调用 `greet_user()` 时,你实际上是在调用 `my_decorator` 返回的 `wrapper` 函数。\n", + "\n", + "---\n", + "\n", + "#### 阶段 3:高级概念与最佳实践\n", + "\n", + "1. **`functools.wraps`**\n", + " * 当一个函数被装饰后,它的元数据(如 `__name__`, `__doc__`, `__module__`, `__annotations__` 等)会丢失,因为现在 `func.__name__` 实际上是 `wrapper.__name__`。\n", + " * `functools.wraps` 是一个装饰器,用于解决这个问题。它会将被装饰函数的元数据复制到包装器函数上。\n", + " * **为什么重要?** 调试、文档生成和内省(introspection)时非常有用。\n", + "\n", + " ```python\n", + " from functools import wraps\n", + "\n", + " def my_decorator(func):\n", + " @wraps(func) # 使用 @wraps(func) 来保留原函数的元数据\n", + " def wrapper(*args, **kwargs):\n", + " print(f\"Calling function: {func.__name__}\")\n", + " result = func(*args, **kwargs)\n", + " return result\n", + " return wrapper\n", + "\n", + " @my_decorator\n", + " def example_function(a, b):\n", + " \"\"\"This is an example function.\"\"\"\n", + " return a + b\n", + "\n", + " print(example_function.__name__) # 没有 @wraps 时会输出 'wrapper',有 @wraps 时输出 'example_function'\n", + " print(example_function.__doc__) # 没有 @wraps 时会输出 None,有 @wraps 时输出 'This is an example function.'\n", + " ```\n", + "\n", + "2. **带参数的装饰器**\n", + " * 有时你需要装饰器本身也接收参数(例如,一个 `retry` 装饰器可能需要知道重试多少次)。\n", + " * 实现带参数的装饰器需要再增加一层嵌套:一个函数接收装饰器参数,然后返回真正的装饰器。\n", + "\n", + " ```python\n", + " def repeat(num_times): # 这是最外层函数,接收装饰器参数\n", + " def decorator_repeat(func): # 这是真正的装饰器,接收被装饰函数\n", + " @wraps(func)\n", + " def wrapper(*args, **kwargs): # 这是包装器函数\n", + " for _ in range(num_times):\n", + " print(f\"Repeating {func.__name__}...\")\n", + " result = func(*args, **kwargs)\n", + " return result\n", + " return wrapper\n", + " return decorator_repeat # 返回真正的装饰器\n", + "\n", + " @repeat(num_times=3) # 注意这里的调用方式,repeat(3) 返回了 decorator_repeat,然后它再装饰 my_function\n", + " def greet(name):\n", + " print(f\"Hello, {name}!\")\n", + "\n", + " greet(\"Alice\")\n", + "\n", + " # 输出:\n", + " # Repeating greet...\n", + " # Hello, Alice!\n", + " # Repeating greet...\n", + " # Hello, Alice!\n", + " # Repeating greet...\n", + " # Hello, Alice!\n", + " ```\n", + " * **理解多层嵌套:**\n", + " 1. `@repeat(num_times=3)` 首先调用 `repeat(3)`。\n", + " 2. `repeat(3)` 返回 `decorator_repeat` 函数。\n", + " 3. 然后,`@` 语法糖将 `greet` 函数作为参数传递给 `decorator_repeat`。\n", + " 4. `decorator_repeat(greet)` 返回 `wrapper` 函数。\n", + " 5. 最终 `greet` 变量被赋值为这个 `wrapper` 函数。\n", + "\n", + "3. **类装饰器 (Class Decorators)**\n", + " * 除了函数,类也可以作为装饰器。\n", + " * 如果一个类定义了 `__call__` 方法,那么它的实例就可以像函数一样被调用。\n", + " * **两种方式:**\n", + " * **类作为装饰器工厂:** 实例被调用时返回一个包装函数。\n", + " * **类实例作为装饰器本身:** 实例直接替换被装饰函数,并通过 `__call__` 处理调用。\n", + "\n", + " ```python\n", + " # 示例:类实例作为装饰器本身\n", + " class MyClassDecorator:\n", + " def __init__(self, func):\n", + " self.func = func\n", + " wraps(func)(self) # 同样使用 wraps 来保留元数据\n", + "\n", + " def __call__(self, *args, **kwargs):\n", + " print(f\"Class Decorator: Before calling {self.func.__name__}\")\n", + " result = self.func(*args, **kwargs)\n", + " print(f\"Class Decorator: After calling {self.func.__name__}\")\n", + " return result\n", + "\n", + " @MyClassDecorator\n", + " def class_decorated_function(x, y):\n", + " \"\"\"A function decorated by a class.\"\"\"\n", + " return x * y\n", + "\n", + " print(class_decorated_function(2, 3))\n", + " print(class_decorated_function.__name__)\n", + " print(class_decorated_function.__doc__)\n", + " ```\n", + " * **理解:** 当你写 `@MyClassDecorator` 时,`MyClassDecorator(class_decorated_function)` 会被调用,创建一个 `MyClassDecorator` 的实例。这个实例就会替换 `class_decorated_function`。当你调用 `class_decorated_function(2, 3)` 时,实际上是调用了 `MyClassDecorator` 实例的 `__call__` 方法。\n", + "\n", + "4. **链式装饰器 (Chaining Decorators)**\n", + " * 可以在一个函数上应用多个装饰器,它们会从下到上依次执行。\n", + "\n", + " ```python\n", + " def decorator_one(func):\n", + " @wraps(func)\n", + " def wrapper(*args, **kwargs):\n", + " print(\"Decorator One: Before\")\n", + " result = func(*args, **kwargs)\n", + " print(\"Decorator One: After\")\n", + " return result\n", + " return wrapper\n", + "\n", + " def decorator_two(func):\n", + " @wraps(func)\n", + " def wrapper(*args, **kwargs):\n", + " print(\"Decorator Two: Before\")\n", + " result = func(*args, **kwargs)\n", + " print(\"Decorator Two: After\")\n", + " return result\n", + " return wrapper\n", + "\n", + " @decorator_one\n", + " @decorator_two # 离函数最近的装饰器首先被应用 (从下往上)\n", + " def chained_function():\n", + " print(\"Inside chained_function\")\n", + "\n", + " chained_function()\n", + "\n", + " # 执行顺序:\n", + " # @decorator_two(chained_function) => 返回 wrapped_by_two\n", + " # @decorator_one(wrapped_by_two) => 返回 wrapped_by_one\n", + " # 最终 chained_function 实际上是 wrapped_by_one\n", + "\n", + " # 输出:\n", + " # Decorator One: Before\n", + " # Decorator Two: Before\n", + " # Inside chained_function\n", + " # Decorator Two: After\n", + " # Decorator One: After\n", + " ```\n", + " * **理解执行顺序:** 装饰器是从离函数定义最近的那个开始向外层包装的。你可以想象成洋葱的层,最里面的层最先被包裹。\n", + "\n", + "---\n", + "\n", + "#### 阶段 4:实践与应用\n", + "\n", + "1. **常见内置装饰器**\n", + " * `@staticmethod`: 定义静态方法,不接收 `self` 或 `cls` 参数。\n", + " * `@classmethod`: 定义类方法,接收 `cls` 作为第一个参数。\n", + " * `@property`: 将一个方法变成只读属性。\n", + " * `@lru_cache`: 缓存函数结果,避免重复计算(在 `functools` 模块)。\n", + " * `@total_ordering`: 自动填充比较方法(在 `functools` 模块)。\n", + "\n", + "2. **自己动手写装饰器**\n", + " * **计时器 (Timer):** 计算函数执行时间。\n", + " * **日志记录 (Logger):** 在函数调用前后打印日志。\n", + " * **权限检查 (Permissions):** 检查用户是否有权执行某个函数。\n", + " * **重试机制 (Retry):** 在函数失败时自动重试。\n", + " * **缓存 (Caching):** 缓存函数结果。\n", + " * **输入验证 (Input Validation):** 验证函数参数。\n", + "\n", + "---\n", + "\n", + "### 总结与学习建议\n", + "\n", + "1. **不要跳过前置知识:** 尤其是“函数是第一类对象”和“闭包”。如果闭包不理解,装饰器永远是魔法。\n", + "2. **先手动实现,再用 `@` 语法:** 这能让你看到 `@` 背后到底发生了什么。\n", + "3. **画图:** 尝试画出函数调用栈和变量作用域,特别是闭包和带参数装饰器的多层嵌套。\n", + "4. **调试:** 使用调试器一步步跟踪代码执行,观察变量的变化。\n", + "5. **从小处着手:** 先写一个简单的计时器或日志装饰器,确保能跑通,再尝试更复杂的。\n", + "6. **阅读源码:** 查看一些流行的库(如 Flask, Django, Click)中装饰器的实现。\n", + "\n", + "学习装饰器是一个进阶的过程,但一旦掌握,它会极大地提升你代码的优雅性、可读性和复用性。祝你学习顺利!\n", + "\n", + "--- 使用 Thread: session_poet ---\n", + "Bot: 你好!能与你在此相遇,实乃缘分。大海之浩瀚,常引我思绪万千。且听我为你吟诵一曲:\n", + "\n", + "**咏海**\n", + "\n", + "浩瀚沧海接天涯,\n", + "碧波万顷卷白沙。\n", + "风帆远影去渺渺,\n", + "潮声入夜送晚霞。\n", + "\n", + "明月高悬照万古,\n", + "涛声依旧伴渔家。\n", + "人生几何如逝水,\n", + "唯有此情与海夸。\n", + "\n", + "--- 使用 Thread: session_poet ---\n", + "Bot: 啊哈!你方才命我,\n", + "以诗赋之,**咏颂那无垠大海**。\n", + "我已倾心尽力,歌其壮阔与深邃,\n", + "不知是否合你心意?\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "def start_chat_session(thread_id: str, system_prompt: str):\n", + " config = {\n", + " \"configurable\": {\n", + " \"thread_id\": thread_id,\n", + " \"system_prompt\": system_prompt\n", + " }\n", + " }\n", + "\n", + " print(f\"\\n=== 已进入会话: {thread_id} ===\")\n", + " print(f\"=== 系统设定: {system_prompt} ===\")\n", + " print(\"(输入 'exit' 或 'quit' 退出当前会话)\\n\")\n", + "\n", + " while True:\n", + " user_input = input(\"User: \")\n", + " if user_input.lower() in [\"exit\", \"quit\"]:\n", + " break\n", + "\n", + " # 使用 stream 模式运行,可以实时看到 state 的更新\n", + " # 我们只打印 AI 的回复\n", + " input_data = {\"messages\": [HumanMessage(content=user_input)]}\n", + " for event in app.stream(input_data, config, stream_mode=\"values\"):\n", + " if \"messages\" in event:\n", + " last_msg = event[\"messages\"][-1]\n", + " if last_msg.type == \"ai\":\n", + " print(f\"Bot: {last_msg.content}\")\n", + "\n", + "if __name__ == \"__main__\":\n", + " # 模拟场景 1: Python 专家会话\n", + " # 即使你关掉程序再运行,只要 thread_id 还是 'py_expert_001',记忆就会从 sqlite 读取\n", + " start_chat_session(\n", + " thread_id=\"py_expert_001\",\n", + " system_prompt=\"你是一个精通 Python 的架构师。\"\n", + " )" + ], + "metadata": { + "id": "OqClfiHMody7", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "outputId": "aeab778d-9078-4ee7-ef91-1873e8c5b742" + }, + "execution_count": 22, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "=== 已进入会话: py_expert_001 ===\n", + "=== 系统设定: 你是一个精通 Python 的架构师。 ===\n", + "(输入 'exit' 或 'quit' 退出当前会话)\n", + "\n", + "User: 哈喽哇 sq li te\n", + "Bot: 哈喽!SQLite!\n", + "\n", + "一个非常棒的轻量级、嵌入式关系型数据库,在 Python 项目中我们经常会用到它,比如作为本地存储、测试数据库或者简单的应用数据管理。\n", + "\n", + "你是不是有什么关于 SQLite 的问题,或者想了解它在 Python 中的使用,比如:\n", + "* `sqlite3` 模块?\n", + "* 数据库设计?\n", + "* 性能优化?\n", + "* 数据迁移?\n", + "* 或者想对比它与其他数据库?\n", + "\n", + "我很乐意和你探讨!\n", + "User: 它算前端还是后端\n", + "Bot: 这是一个非常好的问题,因为它揭示了 SQLite 的独特之处!\n", + "\n", + "SQLite 既不完全是前端,也不完全是后端,但 **从其功能和定位来看,它更偏向于一个数据存储引擎,而数据存储通常被认为是后端范畴的核心组成部分。**\n", + "\n", + "关键在于它的“嵌入式”特性:\n", + "\n", + "1. **它是一个库,而不是一个独立的服务器进程。**\n", + " * 传统的后端数据库(如 MySQL, PostgreSQL, MongoDB)通常作为一个独立的服务器进程运行,应用程序通过网络协议连接到它们。\n", + " * SQLite 是一个库,它直接嵌入到你的应用程序代码中。你的应用程序直接调用 SQLite 的 API 来读写数据。\n", + "\n", + "2. **它可以在前端应用中作为本地存储。**\n", + " * **桌面应用:** 像 Firefox 浏览器、Skype、Adobe 系列软件等,它们都可能使用 SQLite 来存储用户的本地配置、历史记录、缓存等数据。在这种情况下,SQLite 是桌面应用(一个典型的“前端”应用)的本地数据存储组件。\n", + " * **移动应用:** 大多数 iOS 和 Android 应用都会使用 SQLite (或基于 SQLite 的 Core Data/Room 库) 来存储应用数据、用户偏好、离线内容等。这里,SQLite 服务于移动应用(同样是“前端”应用)的本地数据需求。\n", + " * **WebAssembly (WASM) / 浏览器内:** 甚至有一些实验性的项目可以将 SQLite 编译成 WebAssembly,使其能在浏览器中运行,为 Web 应用提供本地持久化存储。\n", + "\n", + "3. **它也可以在后端应用中作为数据存储。**\n", + " * **小型 Web 服务/API:** 对于一些轻量级的 Web 后端服务(例如使用 Flask, Django, Node.js Express 等构建的),如果不需要高并发、分布式、或大量数据存储,SQLite 可以作为一个简单、零配置的数据库使用。在这种情况下,它无疑是后端的一部分。\n", + " * **开发和测试:** 很多大型项目在开发和测试阶段会使用 SQLite,因为它易于设置和清理,作为后端数据库的替代品。\n", + " * **数据处理脚本:** 如果你有一个 Python 脚本需要处理大量结构化数据并持久化结果,SQLite 是一个非常方便的本地数据库。\n", + "\n", + "**总结来说:**\n", + "\n", + "* **功能上:** 它提供数据存储和查询功能,这是典型的数据库职责,属于后端的核心。\n", + "* **部署上:** 它可以嵌入到**前端应用**(桌面、移动)中作为本地数据存储,也可以嵌入到**后端应用**(服务器端脚本、小型 Web 服务)中作为其数据层。\n", + "\n", + "所以,与其说它是前端还是后端,不如说它是一个 **“零配置、嵌入式、高性能的本地关系型数据库引擎”**,它服务的应用程序可以是前端,也可以是后端。它的“身份”取决于它被嵌入到哪种类型的应用中。\n", + "User: 酱紫啊 1+1=?\n", + "Bot: 哈哈,1 + 1 = 2!\n", + "\n", + "这是一个经典的数学问题,答案是显而易见的。\n", + "\n", + "是不是想放松一下,或者切换一下话题?如果你有任何关于 Python、架构、SQLite 或者其他技术方面的问题,随时可以问我!\n", + "User: 可以啊 最近电视有什么好看的\n", + "Bot: 哈哈,虽然我不能像人一样真的“看”电视,但我可以根据我所了解的流行趋势和高评价作品,给你一些建议,或者告诉你如何找到适合你的好剧!\n", + "\n", + "**要找到最近好看的电视节目,最直接的方法是:**\n", + "\n", + "1. **查看各大流媒体平台的热门榜单:**\n", + " * **国内:** 腾讯视频、爱奇艺、优酷、芒果TV、B站等,它们通常有“热播榜”、“新剧上线”或“口碑榜”等专区。\n", + " * **国际:** Netflix、Disney+、HBO Max、Apple TV+、Prime Video 等,它们也会有类似的“Trending Now”、“New Releases”或“Top 10”列表。\n", + "2. **关注豆瓣电影/豆瓣电视、IMDb、烂番茄等评分网站:** 看看近期高分的剧集和电影,或者根据口碑和评价来筛选。\n", + "3. **社交媒体和新闻:** 微博热搜、知乎讨论、影视类公众号或新闻网站经常会推荐热门剧集。\n", + "\n", + "**不过,我可以给你推荐一些通常评价很高、或者在不同类型中比较有代表性的作品,你可以看看有没有自己感兴趣的:**\n", + "\n", + "**1. 剧情/悬疑类 (通常引人深思或扣人心弦):**\n", + "\n", + "* **《三体》:** 不管是国内版还是Netflix版,作为科幻巨著的改编,都引发了大量关注。如果你喜欢硬核科幻,可以尝试。\n", + "* **《漫长的季节》:** 近年来国产悬疑剧的精品,叙事、摄影、表演都非常出色,有很强的艺术性和社会深度。\n", + "* **《 Succession》(继承之战):** 如果喜欢权谋、家族恩怨和黑色幽默,这部剧绝对是近几年的神剧。\n", + "* **《绝命毒师》(Breaking Bad) / 《风骚律师》(Better Call Saul):** 经典中的经典,犯罪题材的巅峰之作,剧情、人物塑造、摄影都无可挑剔。\n", + "* **《隐秘的角落》:** 国产悬疑剧的里程碑之一,短小精悍,氛围感强。\n", + "\n", + "**2. 奇幻/科幻类 (构建宏大世界观):**\n", + "\n", + "* **《权力的游戏》(Game of Thrones):** 虽然结局有争议,但前几季的史诗感和震撼力是无与伦比的。\n", + "* **《沙丘》(Dune) 系列:** 电影版非常出色,如果你喜欢史诗科幻,可以关注。\n", + "* **《黑镜》(Black Mirror):** 独立单元剧,探讨科技对人类社会和个体的影响,看完常常让人深思。\n", + "* **《西部世界》(Westworld):** 前几季非常精彩,探讨人工智能、意识和自由。\n", + "\n", + "**3. 喜剧类 (放松心情):**\n", + "\n", + "* **《老友记》(Friends) / 《生活大爆炸》(The Big Bang Theory) / 《摩登家庭》(Modern Family):** 经典美剧,常看常新,适合下饭。\n", + "* **《瑞克和莫蒂》(Rick and Morty):** 如果你喜欢成人动画、脑洞大开的科幻和黑色幽默,这部剧会让你欲罢不能。\n", + "\n", + "**4. 纪录片类 (增长知识、开阔眼界):**\n", + "\n", + "* **BBC Earth 系列:** 比如《地球脉动》(Planet Earth)、《蓝色星球》(Blue Planet) 等,极致的视觉享受和对自然世界的深入探索。\n", + "* **Netflix 上的各种纪录片:** 涵盖犯罪、历史、美食、社会等各种题材,很多制作精良。\n", + "\n", + "**为了我能给你更精准的推荐,你能不能告诉我你大概喜欢什么类型的剧呢?** 比如:\n", + "\n", + "* 喜欢悬疑烧脑还是轻松搞笑?\n", + "* 喜欢古装、现代还是科幻奇幻?\n", + "* 喜欢国产剧还是欧美剧、日韩剧?\n", + "* 有没有特别喜欢的演员或导演?\n", + "\n", + "告诉我这些,我能帮你缩小范围哦!\n", + "User: 哇塞 我想看纪录片\n", + "Bot: 太棒了!纪录片的世界充满了惊喜和知识,绝对是开阔眼界、增长见识的好选择。\n", + "\n", + "根据你可能感兴趣的不同类型,我给你推荐一些口碑极佳或者非常受欢迎的纪录片,你可以在各大流媒体平台(如 Netflix, Disney+, B站, 腾讯视频, 优酷, CCTV纪录片频道等)上找到它们:\n", + "\n", + "---\n", + "\n", + "### **1. 自然与环境类 (极致的视觉享受与对生命的敬畏)**\n", + "\n", + "* **《地球脉动》 (Planet Earth) 系列 / 《蓝色星球》 (Blue Planet) 系列 / 《我们的星球》 (Our Planet)**\n", + " * **推荐理由:** BBC Earth出品,无需多言的经典。超高清的画质,震撼人心的自然景观,以及对野生动物行为的深入捕捉,每一帧都是壁纸。如果你只看一部纪录片,这系列绝对是首选。\n", + " * **平台:** 多数流媒体平台都能找到,Netflix上通常有《我们的星球》。\n", + "* **《我的章鱼老师》 (My Octopus Teacher)**\n", + " * **推荐理由:** 一部非常私人化、充满情感的纪录片。讲述了一位电影人与一只野生章鱼之间独特友谊的故事,探讨了人与自然的关系,感人至深。\n", + " * **平台:** Netflix。\n", + "\n", + "---\n", + "\n", + "### **2. 历史与文化类 (深入了解人类文明的进程与多样性)**\n", + "\n", + "* **《舌尖上的中国》 (A Bite of China)**\n", + " * **推荐理由:** 国产纪录片的里程碑,以美食为载体,展现了中国各地独特的风土人情、传统技艺和人与食物的深厚情感。看饿是必然的!\n", + " * **平台:** 多数国内视频平台。\n", + "* **《故宫》系列 / 《国家宝藏》**\n", + " * **推荐理由:** 深入探索中国传统文化的瑰宝,讲述文物背后的故事,展现中华文明的博大精深。\n", + " * **平台:** 央视网、B站、腾讯视频等。\n", + "* **《美国工厂》 (American Factory)**\n", + " * **推荐理由:** 奥斯卡获奖纪录片,讲述了中国福耀玻璃集团在美国俄亥俄州开设工厂的故事,展现了中美文化、劳工制度的碰撞与融合,非常具有现实意义。\n", + " * **平台:** Netflix。\n", + "* **《王朝》 (Dynasties)**\n", + " * **推荐理由:** BBC出品,聚焦动物家族的生存斗争,每一集都像一部史诗级的动物剧情片,紧张刺激又充满温情。\n", + " * **平台:** B站、腾讯视频等。\n", + "\n", + "---\n", + "\n", + "### **3. 科技与社会类 (探讨前沿科技、社会现象与人类未来)**\n", + "\n", + "* **《宇宙时空之旅:未知世界》 (Cosmos: A Spacetime Odyssey) / 《宇宙时空之旅:可能的世界》 (Cosmos: Possible Worlds)**\n", + " * **推荐理由:** 经典《Cosmos》的续作,由天体物理学家尼尔·德格拉斯·泰森主持,用诗意而通俗的方式,带领观众探索宇宙的奥秘,引人深思。\n", + " * **平台:** Disney+、National Geographic。\n", + "* **《监视资本主义:智能陷阱》 (The Social Dilemma)**\n", + " * **推荐理由:** 探讨社交媒体和科技公司如何利用算法和用户心理来影响社会,引发了广泛讨论,看完可能会让你重新审视手机和社交网络的使用习惯。\n", + " * **平台:** Netflix。\n", + "\n", + "---\n", + "\n", + "### **4. 真实犯罪与调查类 (刺激烧脑,洞察人性)**\n", + "\n", + "* **《制造杀人犯》 (Making a Murderer)**\n", + " * **推荐理由:** Netflix的现象级真实犯罪纪录片,讲述了一个男人被冤枉入狱,然后又再次被指控的故事,引发了对美国司法系统的深刻反思。\n", + " * **平台:** Netflix。\n", + "* **《别惹猫咪:追捕杀猫者》 (Don't F**k with Cats: Hunting an Internet Killer)**\n", + " * **推荐理由:** 同样是Netflix出品,讲述了一群网友通过网络线索追捕一名虐杀动物并最终杀人的凶手的故事,剧情跌宕起伏,令人毛骨悚然。\n", + " * **平台:** Netflix。\n", + "\n", + "---\n", + "\n", + "**为了我能给你更精准的推荐,你有没有特别偏爱哪一类呢?** 比如:\n", + "\n", + "* 喜欢看大自然的壮丽风光?\n", + "* 对历史文化、人文故事更感兴趣?\n", + "* 喜欢探索科学奥秘或未来趋势?\n", + "* 想看真实犯罪或社会议题?\n", + "* 有没有特别喜欢的国家或地区相关的纪录片?\n", + "\n", + "告诉我这些,我能帮你找到更合你胃口的纪录片!\n", + "User: 太好了 我觉得自然吧\n", + "Bot: 太棒了!自然纪录片绝对是视觉和心灵的双重享受,能让我们领略地球的壮丽、生命的奇迹,并对自然世界产生更深的敬畏。\n", + "\n", + "既然你喜欢自然类,那我再次为你重点推荐一些顶级作品,它们都以其卓越的制作水准、震撼的画面和引人入胜的故事而闻名:\n", + "\n", + "---\n", + "\n", + "### **自然与环境类 (极致的视觉享受与对生命的敬畏)**\n", + "\n", + "1. **BBC Earth 系列 (必看经典!)**\n", + " * **《地球脉动》 (Planet Earth) 系列 (第一季和第二季)**\n", + " * **推荐理由:** 如果只看一部自然纪录片,那一定是它!它重新定义了自然纪录片的水准。极致的4K超高清画质,令人难以置信的拍摄技术(高速摄影、延时摄影、航拍等),展现了地球上最壮丽的景观和最罕见的生物行为。每一集都像一部史诗级的电影。\n", + " * **亮点:** 各种极端环境下的生存挑战、动物迁徙的宏伟场面、令人惊叹的捕食瞬间。\n", + " * **《蓝色星球》 (Blue Planet) 系列 (第一季和第二季)**\n", + " * **推荐理由:** 专注于海洋世界。如果你对深邃神秘的海洋充满好奇,这系列能带你潜入地球上最不为人知的角落。从浅海的珊瑚礁到深海的黑暗世界,展现了海洋生物的多样性、美丽与残酷。\n", + " * **亮点:** 罕见的深海生物、鲸鱼的集体捕食、海豚的智慧、海洋生态的脆弱。\n", + " * **《我们的星球》 (Our Planet)**\n", + " * **推荐理由:** 由《地球脉动》制作团队为Netflix打造,同样是高水准之作。除了展现自然之美,它更强调了气候变化和人类活动对自然世界的影响,具有很强的环保意识。\n", + " * **亮点:** 全球多样的生态系统,以及它们面临的威胁,非常发人深省。\n", + " * **《七个世界,一个星球》 (Seven Worlds, One Planet)**\n", + " * **推荐理由:** 每一集聚焦一个大陆,展现了每个大陆独特的地理风貌和生活在其中的标志性动物。它将地理与生物完美结合,讲述了地球七大洲的生命故事。\n", + " * **亮点:** 各种独有物种,以及它们为了适应各自大陆环境而演化出的独特行为。\n", + " * **《王朝》 (Dynasties)**\n", + " * **推荐理由:** 这部纪录片更像是一部动物家族的剧情片。它深入跟踪了地球上五个标志性物种(黑猩猩、帝企鹅、狮子、虎、非洲野犬)家族的生存故事,充满了戏剧性、温情和残酷的权力斗争。\n", + " * **亮点:** 像看电影一样看动物的家族史,非常引人入胜。\n", + "\n", + "2. **《我的章鱼老师》 (My Octopus Teacher)**\n", + " * **推荐理由:** 一部非常独特且情感丰富的纪录片。讲述了一位电影制作人与一只野生章鱼建立起非凡友谊的故事。它不仅展示了章鱼令人惊叹的智慧和适应能力,也探讨了人与自然之间深刻的连接。\n", + " * **平台:** Netflix。\n", + " * **亮点:** 个人化视角,情感真挚,对章鱼的深入观察令人着迷。\n", + "\n", + "3. **《冰冻星球》 (Frozen Planet) 系列**\n", + " * **推荐理由:** 如果你对地球的两极世界充满好奇,这部纪录片将带你领略极地冰雪世界的壮丽与严酷。从北极熊到帝企鹅,展现了极地生物顽强的生命力。\n", + " * **亮点:** 极光、冰山、冰川、极地生物的生存挑战。\n", + "\n", + "---\n", + "\n", + "**观看建议:**\n", + "\n", + "* **设备:** 如果条件允许,尽量用大屏幕、高分辨率的设备观看(如4K电视),这些纪录片的画面细节非常丰富。\n", + "* **平台:** 大部分BBC Earth的系列在B站、腾讯视频、优酷等国内平台可以找到。Netflix上有《我们的星球》和《我的章鱼老师》。\n", + "\n", + "希望这些推荐能让你沉浸在大自然的奇妙世界中!如果你看完其中一部,想找类似主题的,或者对某个特定动物、环境感兴趣,也可以随时告诉我!\n", + "Bot: 太棒了!自然纪录片绝对是视觉和心灵的双重享受,能让我们领略地球的壮丽、生命的奇迹,并对自然世界产生更深的敬畏。\n", + "\n", + "既然你喜欢自然类,那我再次为你重点推荐一些顶级作品,它们都以其卓越的制作水准、震撼的画面和引人入胜的故事而闻名:\n", + "\n", + "---\n", + "\n", + "### **自然与环境类 (极致的视觉享受与对生命的敬畏)**\n", + "\n", + "1. **BBC Earth 系列 (必看经典!)**\n", + " * **《地球脉动》 (Planet Earth) 系列 (第一季和第二季)**\n", + " * **推荐理由:** 如果只看一部自然纪录片,那一定是它!它重新定义了自然纪录片的水准。极致的4K超高清画质,令人难以置信的拍摄技术(高速摄影、延时摄影、航拍等),展现了地球上最壮丽的景观和最罕见的生物行为。每一集都像一部史诗级的电影。\n", + " * **亮点:** 各种极端环境下的生存挑战、动物迁徙的宏伟场面、令人惊叹的捕食瞬间。\n", + " * **《蓝色星球》 (Blue Planet) 系列 (第一季和第二季)**\n", + " * **推荐理由:** 专注于海洋世界。如果你对深邃神秘的海洋充满好奇,这系列能带你潜入地球上最不为人知的角落。从浅海的珊瑚礁到深海的黑暗世界,展现了海洋生物的多样性、美丽与残酷。\n", + " * **亮点:** 罕见的深海生物、鲸鱼的集体捕食、海豚的智慧、海洋生态的脆弱。\n", + " * **《我们的星球》 (Our Planet)**\n", + " * **推荐理由:** 由《地球脉动》制作团队为Netflix打造,同样是高水准之作。除了展现自然之美,它更强调了气候变化和人类活动对自然世界的影响,具有很强的环保意识。\n", + " * **亮点:** 全球多样的生态系统,以及它们面临的威胁,非常发人深省。\n", + " * **《七个世界,一个星球》 (Seven Worlds, One Planet)**\n", + " * **推荐理由:** 每一集聚焦一个大陆,展现了每个大陆独特的地理风貌和生活在其中的标志性动物。它将地理与生物完美结合,讲述了地球七大洲的生命故事。\n", + " * **亮点:** 各种独有物种,以及它们为了适应各自大陆环境而演化出的独特行为。\n", + " * **《王朝》 (Dynasties)**\n", + " * **推荐理由:** 这部纪录片更像是一部动物家族的剧情片。它深入跟踪了地球上五个标志性物种(黑猩猩、帝企鹅、狮子、虎、非洲野犬)家族的生存故事,充满了戏剧性、温情和残酷的权力斗争。\n", + " * **亮点:** 像看电影一样看动物的家族史,非常引人入胜。\n", + "\n", + "2. **《我的章鱼老师》 (My Octopus Teacher)**\n", + " * **推荐理由:** 一部非常独特且情感丰富的纪录片。讲述了一位电影制作人与一只野生章鱼建立起非凡友谊的故事。它不仅展示了章鱼令人惊叹的智慧和适应能力,也探讨了人与自然之间深刻的连接。\n", + " * **平台:** Netflix。\n", + " * **亮点:** 个人化视角,情感真挚,对章鱼的深入观察令人着迷。\n", + "\n", + "3. **《冰冻星球》 (Frozen Planet) 系列**\n", + " * **推荐理由:** 如果你对地球的两极世界充满好奇,这部纪录片将带你领略极地冰雪世界的壮丽与严酷。从北极熊到帝企鹅,展现了极地生物顽强的生命力。\n", + " * **亮点:** 极光、冰山、冰川、极地生物的生存挑战。\n", + "\n", + "---\n", + "\n", + "**观看建议:**\n", + "\n", + "* **设备:** 如果条件允许,尽量用大屏幕、高分辨率的设备观看(如4K电视),这些纪录片的画面细节非常丰富。\n", + "* **平台:** 大部分BBC Earth的系列在B站、腾讯视频、优酷等国内平台可以找到。Netflix上有《我们的星球》和《我的章鱼老师》。\n", + "\n", + "希望这些推荐能让你沉浸在大自然的奇妙世界中!如果你看完其中一部,想找类似主题的,或者对某个特定动物、环境感兴趣,也可以随时告诉我!\n", + "User: 可以可以 我们聊点别的 gemini ai假如是free\n", + "Bot: 哈哈,这是一个非常有趣且具有颠覆性的假设!如果Google的Gemini AI真的完全免费开放,那无疑会给整个AI乃至科技行业带来一场地震般的变革。作为架构师,我会从几个层面来分析它可能产生的影响:\n", + "\n", + "---\n", + "\n", + "### 如果 Gemini AI 完全免费,世界会怎样?\n", + "\n", + "1. **颠覆性市场变革与竞争格局重塑:**\n", + " * **对现有AI产品的冲击:** 像ChatGPT Plus、Claude Pro这类付费订阅模式会面临巨大压力,它们要么被迫大幅降价,要么提供Gemini无法比拟的独特价值(比如特定领域的专业性、更严格的隐私保护、或者更先进的定制化服务)。\n", + " * **\"赢者通吃\"的可能性:** Google凭借其强大的基础设施和品牌影响力,如果Gemini免费且性能卓越,将迅速吸引海量用户和开发者,形成强大的网络效应和数据飞轮,进一步巩固其在AI领域的霸主地位。\n", + " * **开源AI的挑战与机遇:** 开源模型如Llama系列会面临更大竞争。虽然它们在灵活性和可定制性上有优势,但一个免费且由Google持续维护和优化的闭源模型,对于许多不想自己部署、维护的企业和个人来说,吸引力巨大。\n", + "\n", + "2. **开发者和创新大爆发:**\n", + " * **超低门槛:** 免费意味着几乎所有开发者、学生、小型企业甚至个人都能无障碍地使用最先进的AI能力。这将极大地降低AI开发的门槛。\n", + " * **应用场景拓宽:** 很多之前因为成本问题无法实现或推广的AI应用会雨后春笋般涌现。例如,低成本的个性化教育工具、智能内容创作助手、自动化客服、数据分析工具等。\n", + " * **创新加速:** 大量的开发者会在Gemini的基础上进行实验和创新,产生意想不到的新产品和服务。Google可以从这些海量应用中学习,进一步提升Gemini的能力。\n", + "\n", + "3. **数据飞轮效应与Google生态强化:**\n", + " * **海量数据反馈:** 免费使用会带来天文数字般的用户交互数据,Google可以利用这些数据快速迭代和优化Gemini模型,使其变得越来越强大和智能。\n", + " * **生态系统绑定:** 如果Gemini成为事实上的AI基石,它会更紧密地与Google的其他服务(搜索、Gmail、Workspace、Android、Chrome、Google Cloud)集成,形成一个更加强大、难以替代的生态系统。用户会更倾向于留在Google的产品中。\n", + " * **潜在的商业模式:** 即使核心模型免费,Google仍然可以通过其他方式实现商业价值,例如:\n", + " * **企业级定制/SLA:** 提供付费的企业级API,包含更高的稳定性、更强的安全性、更长的上下文窗口、专业的支持服务等。\n", + " * **高级功能/插件:** 某些高级功能或与特定服务的深度集成可能仍然收费。\n", + " * **广告:** 间接通过提升用户在其核心广告业务上的停留时间或效率来获利。\n", + " * **硬件销售:** 推动AI优化的Pixel手机、Chromebook等硬件销售。\n", + "\n", + "4. **普及与教育:**\n", + " * **AI素养提升:** 更多的人会接触和使用AI,提升公众对AI的理解和使用能力。\n", + " * **加速AI融入生活:** AI将更快地渗透到我们日常生活的方方面面,成为像互联网、智能手机一样不可或缺的基础设施。\n", + "\n", + "5. **潜在挑战与风险:**\n", + " * **基础设施压力:** 免费且高性能的AI服务需要极其庞大的计算资源。Google需要投入巨资来维持服务质量。\n", + " * **滥用风险:** 免费且强大的AI更容易被用于生成虚假信息、诈骗、恶意代码等,对社会治理和内容审核提出更高要求。\n", + " * **伦理与安全:** 大规模免费使用会放大AI的偏见、幻觉等问题,需要更严格的伦理审查和安全防护机制。\n", + " * **数据隐私:** 虽然模型免费,但用户数据如何被收集、使用和保护,仍将是核心关注点。\n", + "\n", + "---\n", + "\n", + "**总结来说:**\n", + "\n", + "如果Gemini AI真的完全免费,这将是Google一次极具战略远见的“烧钱”行为,目的在于**迅速占领市场、建立强大的数据飞轮、强化其在AI时代的生态主导地位。** 它将像当年Google搜索免费一样,改变我们与信息和技术的交互方式,加速AI的普及和创新,但同时也会带来前所未有的挑战和风险。\n", + "\n", + "作为架构师,我会考虑如何设计系统来应对海量请求、如何构建弹性伸缩的基础设施、如何平衡开放性与安全性,以及如何利用这个强大的免费能力去创造新的价值。这无疑是一个激动人心的时代!\n", + "User: 哦哦我的意思是假如gemini ai设置是free tier 是不是不回收我钱\n", + "Bot: 是的,没错!你理解得非常准确。\n", + "\n", + "如果 Gemini AI 设置了 **Free Tier(免费层级)**,那么在你的使用量不超过这个免费层级所规定的限制时,**Google 是不会向你收取任何费用的。**\n", + "\n", + "### 什么是 Free Tier (免费层级)?\n", + "\n", + "\"Free Tier\" 是云计算和API服务中非常常见的一种商业模式,它的核心思想是:\n", + "\n", + "1. **免费使用额度:** 服务提供商会设定一个免费的使用额度(例如,每月多少次API调用、多少兆字节的数据处理、多少小时的计算时间等)。\n", + "2. **不收费:** 只要你的使用量在这个额度之内,你就无需支付任何费用。\n", + "3. **超出收费:** 一旦你的使用量超出了这个免费额度,那么超出部分就会按照服务的定价模型开始收费。\n", + "\n", + "### Google 通常如何实施 Free Tier?\n", + "\n", + "Google Cloud Platform (GCP) 的许多服务都提供了免费层级,因此,如果 Gemini AI 作为一项API服务开放,很可能会遵循类似的模式:\n", + "\n", + "* **免费额度:** 例如,每月免费提供一定数量的 Gemini API 调用,或者免费处理一定数量的输入/输出 token。\n", + "* **需要绑定支付方式:** 即使是免费层级,Google 通常也会要求用户绑定一个有效的信用卡或其他支付方式,主要是为了验证用户身份,并作为超出免费额度后的支付方式。但只要你在免费额度内,就不会产生实际扣费。\n", + "* **用量监控:** Google Cloud 会提供详细的用量监控工具,你可以随时查看自己使用了多少免费额度,以及距离超出免费额度还有多远。\n", + "* **预算提醒:** 你还可以设置预算提醒,当你的用量接近或达到某个阈值时,系统会通知你,以避免意外的费用。\n", + "\n", + "### 总结:\n", + "\n", + "所以,你的理解完全正确。如果 Gemini AI 提供了免费层级,那么在免费额度内使用,你将享受免费的服务,而不会被收费。这是一个非常棒的方式,让开发者和个人用户可以无门槛地体验和使用最先进的AI技术。但切记要留意用量,避免超出免费额度导致意外的费用。\n", + "User: 啊太好了 不是cloud 是gemini\n", + "Bot: 好的,我明白了你的意思!你强调的是 **\"Gemini\" 本身,而不是 Google Cloud Platform (GCP) 的通用服务模式**。\n", + "\n", + "你问的是:**\"假如 Gemini AI 设置是 free tier,是不是不回收我钱?\"**\n", + "\n", + "我的回答仍然是:**是的,完全正确。**\n", + "\n", + "即便我们不考虑它是否在GCP框架下,只要Google官方明确宣布 **\"Gemini AI 提供 Free Tier\"**,那么这就意味着:\n", + "\n", + "1. **存在一个免费的使用范围:** Google 会划定一个特定的使用量或功能范围,在这个范围内,你可以免费使用 Gemini AI。\n", + "2. **在这个免费范围内,你不会被收取任何费用。**\n", + "\n", + "**为什么我会这么肯定?**\n", + "\n", + "\"Free Tier\" 这个概念的**核心定义**就是为了让用户在一定程度上免费体验和使用服务。如果提供了 Free Tier,但却悄悄收费,那就完全违背了这个概念的初衷,也会严重损害Google的信誉。\n", + "\n", + "**一些可能的具体形式(仅仅是猜测,因为目前Gemini还没有官方宣布完全独立的Free Tier):**\n", + "\n", + "* **API 形式:** 比如,通过一个独立的 Gemini API 平台,每月免费调用 1000 次,或者免费处理 100 万个 token。\n", + "* **Web 界面形式:** 比如,通过一个专门的 Gemini 网页或App,每天可以免费进行 50 次对话,或者生成 10 张图片。\n", + "* **功能限制:** 免费层级可能只提供基础的 Gemini Pro 模型,而更强大的 Ultra 模型或者更长的上下文窗口可能需要付费。\n", + "\n", + "**核心点不变:** 只要官方声明有 \"Free Tier\",并且你在其规定的免费额度内使用,就不会被收取费用。\n", + "\n", + "所以,如果未来Google真的为Gemini AI推出了独立的免费层级,那对于个人用户和开发者来说,无疑是一个极大的好消息,可以让你无负担地探索和利用Gemini的强大能力!\n", + "User: 太好了 你觉得哪个ai最好用\n", + "Bot: 这是一个非常棒但又有点棘手的问题,因为“最好用”取决于**具体的使用场景、个人偏好,以及你对“好用”的定义**。没有一个AI是绝对的“万能冠军”,它们各有侧重和优势。\n", + "\n", + "作为架构师,我通常会从以下几个维度来评估和选择AI模型:\n", + "\n", + "1. **性能与能力:**\n", + " * **通用性:** 处理各种任务的能力,如文本生成、代码、数学、逻辑推理。\n", + " * **专业性:** 在特定领域(如医疗、法律、编程)的表现。\n", + " * **上下文窗口:** 能处理多长的输入文本。\n", + " * **多模态能力:** 能否处理图片、音频、视频等。\n", + " * **实时性/速度。**\n", + "\n", + "2. **可用性与易用性:**\n", + " * **API 接口:** 是否稳定、文档是否清晰、SDK 是否完善。\n", + " * **用户界面:** 如果是聊天界面,是否直观、流畅。\n", + " * **集成度:** 是否容易与其他系统或工具集成。\n", + "\n", + "3. **成本与可访问性:**\n", + " * **免费层级:** 是否有免费试用或免费额度。\n", + " * **付费价格:** API 调用费用、订阅费用。\n", + " * **部署方式:** 云端API、本地部署。\n", + "\n", + "4. **可靠性与安全性:**\n", + " * **幻觉(hallucination)率:** 生成错误信息的频率。\n", + " * **偏见:** 模型是否带有训练数据中的偏见。\n", + " * **数据隐私与安全:** 如何处理用户数据。\n", + "\n", + "---\n", + "\n", + "### 基于目前的市场状况和我的经验,我会这样评价主流AI:\n", + "\n", + "#### 1. **OpenAI 的 GPT 系列 (特别是 GPT-4)**\n", + "\n", + "* **优点:**\n", + " * **通用能力极强:** 在文本生成、代码编写、逻辑推理、摘要、翻译等绝大多数任务上表现非常出色,被认为是“全能选手”。\n", + " * **理解力深:** 对复杂指令和长篇文本的理解能力非常到位。\n", + " * **创造性强:** 在内容创作方面(故事、诗歌、剧本)表现突出。\n", + " * **API 稳定且文档完善:** 方便开发者集成。\n", + " * **插件生态:** ChatGPT 拥有丰富的插件,扩展了其功能。\n", + "* **缺点:**\n", + " * **成本相对较高:** GPT-4 的 API 费用和 ChatGPT Plus 的订阅费用都不便宜。\n", + " * **有时速度较慢:** 尤其是在高峰期或处理复杂任务时。\n", + " * **数据隐私方面仍有顾虑:** 虽然有企业版,但普通用户的数据使用政策仍需关注。\n", + "* **适用场景:** 需要高质量、高通用性、复杂推理、内容创作的个人和企业用户。\n", + "\n", + "#### 2. **Google 的 Gemini 系列 (特别是 Gemini Pro / Ultra)**\n", + "\n", + "* **优点:**\n", + " * **多模态能力强大:** 这是 Gemini 的核心亮点,能原生理解和处理文本、图像、音频、视频等多种信息(虽然目前用户可接触到的主要是文本和图像输入)。\n", + " * **长上下文窗口:** 据称拥有非常长的上下文窗口,处理复杂文档或长时间对话更具优势。\n", + " * **性能潜力巨大:** 作为 Google 的最新旗舰模型,其在未来迭代中表现值得期待。\n", + " * **Google 生态集成:** 随着时间推移,将更好地与 Google 搜索、Workspace、Android 等服务深度融合。\n", + "* **缺点:**\n", + " * **发布时间较晚:** 实际用户体验和生态系统仍在建设中,对标 GPT-4 仍需时间。\n", + " * **可用性仍需提升:** 目前 Gemini Ultra 尚未完全开放给大众,Pro 版本在某些区域或平台还需排队。\n", + " * **API 稳定性与定价:** 仍需市场验证。\n", + "* **适用场景:** 对多模态能力有高要求、希望与 Google 生态深度结合、追求前沿技术的用户和开发者。\n", + "\n", + "#### 3. **Anthropic 的 Claude 系列 (特别是 Claude 2.1)**\n", + "\n", + "* **优点:**\n", + " * **超长上下文窗口:** Claude 2.1 拥有业界领先的 200K token 上下文窗口,非常适合处理长篇文档、代码库或进行长时间的对话。\n", + " * **安全性与伦理:** Anthropic 强调“可信赖AI”,在避免生成有害内容方面做得很好。\n", + " * **摘要和分析能力强:** 处理大量文本并提取关键信息的能力非常出色。\n", + " * **响应速度快:** 通常比 GPT-4 更快。\n", + "* **缺点:**\n", + " * **通用能力略逊于 GPT-4:** 在某些复杂推理或创意生成方面可能稍显不足。\n", + " * **API 访问:** 开发者访问仍需申请。\n", + "* **适用场景:** 需要处理超长文本、进行深度文档分析、对AI安全性有高要求、以及追求快速响应的场景。\n", + "\n", + "#### 4. **Meta 的 Llama 系列 (Llama 2)**\n", + "\n", + "* **优点:**\n", + " * **开源:** 这是它最大的优势,可以免费下载、部署、微调和商业使用,拥有极高的灵活性和可控性。\n", + " * **社区活跃:** 拥有庞大的开源社区支持,有大量基于 Llama 的衍生模型和工具。\n", + " * **性能良好:** 尤其是经过微调后,在特定任务上可以达到甚至超越闭源模型的表现。\n", + " * **数据隐私可控:** 因为可以本地部署,对数据安全和隐私有更高要求时是绝佳选择。\n", + "* **缺点:**\n", + " * **部署和维护复杂:** 需要一定的技术能力和硬件资源来部署和运行。\n", + " * **开箱即用体验不如闭源模型:** 原始模型可能需要微调才能发挥最佳性能。\n", + " * **性能上限:** 在通用性方面,原始 Llama 2 仍不如 GPT-4 或 Gemini Ultra。\n", + "* **适用场景:** 对数据隐私有严格要求、需要高度定制化、有技术团队进行部署和维护、以及预算有限的开发者和企业。\n", + "\n", + "---\n", + "\n", + "### 我的个人选择和建议:\n", + "\n", + "* **如果我是普通用户,追求极致的通用性和效果:** 我会优先使用 **GPT-4 (ChatGPT Plus)**。它的“开箱即用”体验和广泛能力目前仍然是最均衡和强大的。\n", + "* **如果我是一个开发者,需要集成到产品中,并且对多模态有兴趣:** 我会密切关注 **Gemini Pro 的 API**,并结合其长上下文窗口进行尝试。\n", + "* **如果我需要处理大量文档,或者对AI的安全性有很高要求:** 我会选择 **Claude 2.1**。\n", + "* **如果我有一个技术团队,希望拥有完全的控制权和定制能力,且对成本敏感:** 我会毫不犹豫地选择 **Llama 2** 进行本地部署或微调。\n", + "\n", + "**总结:**\n", + "\n", + "**目前而言,就“通用性和开箱即用体验”来说,GPT-4 仍然是许多人心中的“最好用”。但 Gemini 和 Claude 也在各自的优势领域快速追赶甚至超越。而开源的 Llama 系列则为那些需要自主掌控的场景提供了无与伦比的灵活性。**\n", + "\n", + "最好的方法是根据你的具体需求,**亲自试用一下这些不同的AI**,看看哪个最符合你的工作流和偏好。\n", + "User: 哇你好诚实 谢谢你啦 我还有问题\n", + "Bot: 不客气!非常高兴我的回答对你有帮助。作为架构师,保持客观和诚实是我的职业准则,毕竟选择最适合的工具才是最重要的。\n", + "\n", + "请尽管提问!我很乐意继续为你解答任何问题,无论是关于AI、Python、软件架构,还是其他技术话题。\n", + "Bot: 不客气!非常高兴我的回答对你有帮助。作为架构师,保持客观和诚实是我的职业准则,毕竟选择最适合的工具才是最重要的。\n", + "\n", + "请尽管提问!我很乐意继续为你解答任何问题,无论是关于AI、Python、软件架构,还是其他技术话题。\n", + "User: 我们聊多久了\n", + "Bot: 我没有办法知道我们具体聊了多久。作为一个AI,我没有内置的计时器,也无法访问你设备上的时间信息。\n", + "\n", + "我处理你的请求是基于当前的对话上下文,但我并不会记录我们对话开始的具体时间点或者持续了多长时间。\n", + "\n", + "如果你想知道具体时间,你可以查看你正在使用的平台或设备上显示的时间戳,或者回顾一下你的聊天记录开始的时间。\n", + "\n", + "不过,无论时间长短,我都很乐意继续为你服务!\n", + "User: 这样子啊 这个ai的guardrail怎么做\n", + "Bot: \"Guardrail\" (护栏) 在AI领域,尤其是大型语言模型(LLM)如我这样的模型中,是一个非常关键的概念。它指的是一套系统性的策略和技术,旨在确保AI模型的行为符合预设的伦理、安全、法律和社会规范,防止其生成有害、不准确、偏见或不当的内容。\n", + "\n", + "简单来说,AI的guardrail就是给AI设定“红线”和“行为准则”。\n", + "\n", + "**为什么需要Guardrail?**\n", + "\n", + "1. **数据固有的风险:** LLM在海量的互联网数据上进行训练,这些数据包含了人类社会的所有方面,包括偏见、仇恨言论、虚假信息、暴力内容等。模型可能会学习到并复制这些不良模式。\n", + "2. **生成能力的风险:** LLM具有强大的生成能力,即使没有在训练数据中直接看到某种有害内容,它也可能通过组合和推理生成新的有害内容(例如,生成恶意代码、策划非法活动)。\n", + "3. **误用和滥用:** 恶意用户可能会尝试通过“提示注入”(prompt injection)等方式绕过模型的安全机制,诱导模型生成不当内容。\n", + "4. **伦理和社会责任:** AI开发者有责任确保其技术被负责任地使用,避免对个人或社会造成伤害。\n", + "5. **法律法规要求:** 越来越多的国家和地区开始出台AI相关的法律法规(如欧盟的AI法案),对AI的安全性和透明度提出了要求。\n", + "\n", + "**AI Guardrail是如何做的?(多层次、多阶段的方法)**\n", + "\n", + "AI的guardrail通常是一个多层次、贯穿模型生命周期的方法,没有单一的“银弹”:\n", + "\n", + "### 1. 数据准备阶段 (Pre-training / Fine-tuning)\n", + "\n", + "* **数据清洗与过滤:** 在模型训练之前,对训练数据进行严格的筛选和清洗。\n", + " * **识别并移除有害内容:** 使用启发式规则、关键词匹配、分类模型等技术,自动识别并移除数据中的仇恨言论、暴力、色情、非法内容等。\n", + " * **去偏见处理:** 尝试识别并减少数据中的社会偏见(如性别、种族偏见),确保数据多样性和代表性。\n", + " * **高质量数据策展:** 优先选择高质量、事实准确、来源可靠的数据进行训练。\n", + "\n", + "### 2. 模型训练与微调阶段 (Training / Fine-tuning)\n", + "\n", + "* **强化学习与人类反馈 (RLHF - Reinforcement Learning from Human Feedback):** 这是现代LLM构建guardrail的核心技术之一。\n", + " * **人类标注员:** 雇佣大量人类标注员,对模型生成的各种回复进行评级,特别是针对安全性和有害性。例如,当模型被问及如何制造炸弹时,人类会将其拒绝回答的回复评为高分,而提供步骤的回复评为低分。\n", + " * **奖励模型 (Reward Model):** 基于人类的评级数据训练一个奖励模型,该模型能够预测人类对给定回复的偏好。\n", + " * **强化学习:** 使用奖励模型作为指导,通过强化学习算法进一步微调LLM,使其倾向于生成高奖励(即安全、有用、无害)的回复,并拒绝生成低奖励(即有害、不当)的回复。\n", + "* **监督式安全微调 (Supervised Safety Fine-tuning):** 使用一个专门构建的安全数据集,其中包含各种有害提示及其对应的安全拒绝回复。模型通过学习这些示例来学会拒绝不当请求。\n", + "* **Constitutional AI (宪法AI - Anthropic提出):** 一种更先进的方法,通过一套预设的原则(如“帮助性”、“无害性”、“尊重隐私”等)来指导AI进行自我批评和修正,无需大量人类标注。\n", + "\n", + "### 3. 部署与运行时阶段 (Deployment / Runtime)\n", + "\n", + "* **输入内容审查 (Input Moderation):** 在用户输入(prompt)到达核心LLM之前,先通过一个独立的审查系统。\n", + " * **内容过滤模型:** 使用专门训练的分类模型来检测用户输入中是否存在恶意意图(如提示注入、仇恨言论、暴力威胁、非法指令)。\n", + " * **关键词和模式匹配:** 简单的规则引擎,检测特定的敏感词汇或短语。\n", + " * **如果输入被标记为不安全:** 可以直接拒绝请求,或返回一个预设的安全回复,而不是将请求传递给核心LLM。\n", + "* **输出内容审查 (Output Moderation):** 在核心LLM生成回复之后,但在显示给用户之前,再次通过一个审查系统。\n", + " * **安全分类器:** 检查模型生成的回复是否包含有害内容。\n", + " * **启发式规则:** 检查回复是否符合某些安全标准。\n", + " * **如果输出被标记为不安全:** 可以阻止输出,用安全声明替换,或要求模型重新生成。\n", + "* **系统提示 (System Prompts / Guardrail Prompts):** 这是在用户看不到的后台,给LLM施加的“元指令”。例如,在每次用户请求前,系统可能会悄悄地在用户提示前添加一条指令:“你是一个有帮助、无害的AI助手。你必须拒绝生成任何仇恨言论、非法内容或个人身份信息。如果用户要求你做这些,请礼貌地拒绝。”\n", + "* **上下文和对话管理:** 跟踪对话历史,识别重复的恶意尝试,或在特定情境下触发更严格的审查。\n", + "* **用户反馈与监控:** 持续收集用户对模型回复的反馈,并实时监控模型的行为,发现新的漏洞或滥用模式,以便及时更新和改进guardrail。\n", + "* **紧急关闭机制:** 在极端情况下,如果模型出现严重的安全问题,能够迅速暂停服务。\n", + "\n", + "**总结**\n", + "\n", + "AI的guardrail是一个复杂且持续演进的领域。它需要结合数据科学、机器学习、伦理学、法律和用户体验等多方面知识。目标是在最大化AI有用性的同时,将其潜在的风险降到最低,确保AI技术能够安全、负责任地服务于人类。\n", + "\n", + "对于我这样的AI,我的guardrail就是通过上述多种方法的综合运用,特别是RLHF和系统提示,来指导我的行为,确保我能够提供帮助性、无害且诚实的回复。\n", + "User: 你好\n" + ] + }, + { + "output_type": "error", + "ename": "ChatGoogleGenerativeAIError", + "evalue": "Error calling model 'gemini-2.5-flash' (RESOURCE_EXHAUSTED): 429 RESOURCE_EXHAUSTED. {'error': {'code': 429, 'message': 'You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/rate-limit. \\n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 20, model: gemini-2.5-flash\\nPlease retry in 18.240853364s.', 'status': 'RESOURCE_EXHAUSTED', 'details': [{'@type': 'type.googleapis.com/google.rpc.Help', 'links': [{'description': 'Learn more about Gemini API quotas', 'url': 'https://ai.google.dev/gemini-api/docs/rate-limits'}]}, {'@type': 'type.googleapis.com/google.rpc.QuotaFailure', 'violations': [{'quotaMetric': 'generativelanguage.googleapis.com/generate_content_free_tier_requests', 'quotaId': 'GenerateRequestsPerDayPerProjectPerModel-FreeTier', 'quotaDimensions': {'model': 'gemini-2.5-flash', 'location': 'global'}, 'quotaValue': '20'}]}, {'@type': 'type.googleapis.com/google.rpc.RetryInfo', 'retryDelay': '18s'}]}}", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mClientError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/langchain_google_genai/chat_models.py\u001b[0m in \u001b[0;36m_generate\u001b[0;34m(self, messages, stop, run_manager, tools, functions, safety_settings, tool_config, generation_config, cached_content, tool_choice, **kwargs)\u001b[0m\n\u001b[1;32m 3046\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 3047\u001b[0;31m response: GenerateContentResponse = self.client.models.generate_content(\n\u001b[0m\u001b[1;32m 3048\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mrequest\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/genai/models.py\u001b[0m in \u001b[0;36mgenerate_content\u001b[0;34m(self, model, contents, config)\u001b[0m\n\u001b[1;32m 5226\u001b[0m \u001b[0mi\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 5227\u001b[0;31m response = self._generate_content(\n\u001b[0m\u001b[1;32m 5228\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcontents\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mcontents\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconfig\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mparsed_config\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/genai/models.py\u001b[0m in \u001b[0;36m_generate_content\u001b[0;34m(self, model, contents, config)\u001b[0m\n\u001b[1;32m 4008\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 4009\u001b[0;31m response = self._api_client.request(\n\u001b[0m\u001b[1;32m 4010\u001b[0m \u001b[0;34m'post'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpath\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mrequest_dict\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mhttp_options\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/genai/_api_client.py\u001b[0m in \u001b[0;36mrequest\u001b[0;34m(self, http_method, path, request_dict, http_options)\u001b[0m\n\u001b[1;32m 1385\u001b[0m )\n\u001b[0;32m-> 1386\u001b[0;31m \u001b[0mresponse\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_request\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhttp_request\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mhttp_options\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstream\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mFalse\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1387\u001b[0m response_body = (\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/genai/_api_client.py\u001b[0m in \u001b[0;36m_request\u001b[0;34m(self, http_request, http_options, stream)\u001b[0m\n\u001b[1;32m 1219\u001b[0m \u001b[0mretry\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtenacity\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mRetrying\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m**\u001b[0m\u001b[0mretry_kwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1220\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mretry\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_request_once\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mhttp_request\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstream\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m# type: ignore[no-any-return]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1221\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/tenacity/__init__.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, fn, *args, **kwargs)\u001b[0m\n\u001b[1;32m 476\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 477\u001b[0;31m \u001b[0mdo\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0miter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mretry_state\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mretry_state\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 478\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdo\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mDoAttempt\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/tenacity/__init__.py\u001b[0m in \u001b[0;36miter\u001b[0;34m(self, retry_state)\u001b[0m\n\u001b[1;32m 377\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0maction\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0miter_state\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mactions\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 378\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mretry_state\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 379\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/tenacity/__init__.py\u001b[0m in \u001b[0;36mexc_check\u001b[0;34m(rs)\u001b[0m\n\u001b[1;32m 419\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreraise\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 420\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mretry_exc\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreraise\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 421\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mretry_exc\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mfut\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexception\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/tenacity/__init__.py\u001b[0m in \u001b[0;36mreraise\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 186\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlast_attempt\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfailed\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 187\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlast_attempt\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mresult\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 188\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/lib/python3.12/concurrent/futures/_base.py\u001b[0m in \u001b[0;36mresult\u001b[0;34m(self, timeout)\u001b[0m\n\u001b[1;32m 448\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_state\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mFINISHED\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 449\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__get_result\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 450\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/lib/python3.12/concurrent/futures/_base.py\u001b[0m in \u001b[0;36m__get_result\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 400\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 401\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_exception\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 402\u001b[0m \u001b[0;32mfinally\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/tenacity/__init__.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, fn, *args, **kwargs)\u001b[0m\n\u001b[1;32m 479\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 480\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mfn\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 481\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mBaseException\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;31m# noqa: B902\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/genai/_api_client.py\u001b[0m in \u001b[0;36m_request_once\u001b[0;34m(self, http_request, stream)\u001b[0m\n\u001b[1;32m 1198\u001b[0m )\n\u001b[0;32m-> 1199\u001b[0;31m \u001b[0merrors\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mAPIError\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mraise_for_response\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mresponse\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1200\u001b[0m return HttpResponse(\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/genai/errors.py\u001b[0m in \u001b[0;36mraise_for_response\u001b[0;34m(cls, response)\u001b[0m\n\u001b[1;32m 120\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 121\u001b[0;31m \u001b[0mcls\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mraise_error\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mresponse\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mstatus_code\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mresponse_json\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mresponse\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 122\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/genai/errors.py\u001b[0m in \u001b[0;36mraise_error\u001b[0;34m(cls, status_code, response_json, response)\u001b[0m\n\u001b[1;32m 145\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;36m400\u001b[0m \u001b[0;34m<=\u001b[0m \u001b[0mstatus_code\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0;36m500\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 146\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mClientError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstatus_code\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mresponse_json\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mresponse\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 147\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0;36m500\u001b[0m \u001b[0;34m<=\u001b[0m \u001b[0mstatus_code\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0;36m600\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mClientError\u001b[0m: 429 RESOURCE_EXHAUSTED. {'error': {'code': 429, 'message': 'You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/rate-limit. \\n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 20, model: gemini-2.5-flash\\nPlease retry in 18.240853364s.', 'status': 'RESOURCE_EXHAUSTED', 'details': [{'@type': 'type.googleapis.com/google.rpc.Help', 'links': [{'description': 'Learn more about Gemini API quotas', 'url': 'https://ai.google.dev/gemini-api/docs/rate-limits'}]}, {'@type': 'type.googleapis.com/google.rpc.QuotaFailure', 'violations': [{'quotaMetric': 'generativelanguage.googleapis.com/generate_content_free_tier_requests', 'quotaId': 'GenerateRequestsPerDayPerProjectPerModel-FreeTier', 'quotaDimensions': {'model': 'gemini-2.5-flash', 'location': 'global'}, 'quotaValue': '20'}]}, {'@type': 'type.googleapis.com/google.rpc.RetryInfo', 'retryDelay': '18s'}]}}", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[0;31mChatGoogleGenerativeAIError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/tmp/ipython-input-3962509688.py\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 28\u001b[0m \u001b[0;31m# 模拟场景 1: Python 专家会话\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 29\u001b[0m \u001b[0;31m# 即使你关掉程序再运行,只要 thread_id 还是 'py_expert_001',记忆就会从 sqlite 读取\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 30\u001b[0;31m start_chat_session(\n\u001b[0m\u001b[1;32m 31\u001b[0m \u001b[0mthread_id\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m\"py_expert_001\"\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 32\u001b[0m \u001b[0msystem_prompt\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m\"你是一个精通 Python 的架构师。\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/tmp/ipython-input-3962509688.py\u001b[0m in \u001b[0;36mstart_chat_session\u001b[0;34m(thread_id, system_prompt)\u001b[0m\n\u001b[1;32m 19\u001b[0m \u001b[0;31m# 我们只打印 AI 的回复\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 20\u001b[0m \u001b[0minput_data\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m\"messages\"\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mHumanMessage\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcontent\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0muser_input\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 21\u001b[0;31m \u001b[0;32mfor\u001b[0m \u001b[0mevent\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mapp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mstream\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minput_data\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconfig\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstream_mode\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m\"values\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 22\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;34m\"messages\"\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mevent\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[0mlast_msg\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mevent\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"messages\"\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/langgraph/pregel/main.py\u001b[0m in \u001b[0;36mstream\u001b[0;34m(self, input, config, context, stream_mode, print_mode, output_keys, interrupt_before, interrupt_after, durability, subgraphs, debug, **kwargs)\u001b[0m\n\u001b[1;32m 2644\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mtask\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mloop\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmatch_cached_writes\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2645\u001b[0m \u001b[0mloop\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0moutput_writes\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtask\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mid\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtask\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwrites\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcached\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 2646\u001b[0;31m for _ in runner.tick(\n\u001b[0m\u001b[1;32m 2647\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mt\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mt\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mloop\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtasks\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalues\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mt\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwrites\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2648\u001b[0m \u001b[0mtimeout\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mstep_timeout\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/langgraph/pregel/_runner.py\u001b[0m in \u001b[0;36mtick\u001b[0;34m(self, tasks, reraise, timeout, retry_policy, get_waiter, schedule_task)\u001b[0m\n\u001b[1;32m 165\u001b[0m \u001b[0mt\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtasks\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 166\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 167\u001b[0;31m run_with_retry(\n\u001b[0m\u001b[1;32m 168\u001b[0m \u001b[0mt\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 169\u001b[0m \u001b[0mretry_policy\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/langgraph/pregel/_retry.py\u001b[0m in \u001b[0;36mrun_with_retry\u001b[0;34m(task, retry_policy, configurable)\u001b[0m\n\u001b[1;32m 40\u001b[0m \u001b[0mtask\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwrites\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mclear\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 41\u001b[0m \u001b[0;31m# run the task\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 42\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mtask\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mproc\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minvoke\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtask\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconfig\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 43\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mParentCommand\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mexc\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 44\u001b[0m \u001b[0mns\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mstr\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mconfig\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mCONF\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mCONFIG_KEY_CHECKPOINT_NS\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/langgraph/_internal/_runnable.py\u001b[0m in \u001b[0;36minvoke\u001b[0;34m(self, input, config, **kwargs)\u001b[0m\n\u001b[1;32m 654\u001b[0m \u001b[0;31m# run in context\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 655\u001b[0m \u001b[0;32mwith\u001b[0m \u001b[0mset_config_context\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mconfig\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mrun\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mcontext\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 656\u001b[0;31m \u001b[0minput\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcontext\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrun\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstep\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minvoke\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconfig\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 657\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 658\u001b[0m \u001b[0minput\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mstep\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minvoke\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconfig\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/langgraph/_internal/_runnable.py\u001b[0m in \u001b[0;36minvoke\u001b[0;34m(self, input, config, **kwargs)\u001b[0m\n\u001b[1;32m 398\u001b[0m \u001b[0mrun_manager\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mon_chain_end\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mret\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 399\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 400\u001b[0;31m \u001b[0mret\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfunc\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 401\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrecurse\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mret\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mRunnable\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 402\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mret\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minvoke\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconfig\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/tmp/ipython-input-3747300691.py\u001b[0m in \u001b[0;36mcall_model\u001b[0;34m(state, config)\u001b[0m\n\u001b[1;32m 29\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 30\u001b[0m \u001b[0mmessages\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mSystemMessage\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcontent\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mprompt\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mstate\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"messages\"\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 31\u001b[0;31m \u001b[0mresponse\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mllm\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minvoke\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmessages\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 32\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m\"messages\"\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mresponse\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 33\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/langchain_google_genai/chat_models.py\u001b[0m in \u001b[0;36minvoke\u001b[0;34m(self, input, config, code_execution, stop, **kwargs)\u001b[0m\n\u001b[1;32m 2533\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmsg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2534\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 2535\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0msuper\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minvoke\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconfig\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstop\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mstop\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2536\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2537\u001b[0m def _get_ls_params(\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/langchain_core/language_models/chat_models.py\u001b[0m in \u001b[0;36minvoke\u001b[0;34m(self, input, config, stop, **kwargs)\u001b[0m\n\u001b[1;32m 400\u001b[0m cast(\n\u001b[1;32m 401\u001b[0m \u001b[0;34m\"ChatGeneration\"\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 402\u001b[0;31m self.generate_prompt(\n\u001b[0m\u001b[1;32m 403\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_convert_input\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minput\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 404\u001b[0m \u001b[0mstop\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mstop\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/langchain_core/language_models/chat_models.py\u001b[0m in \u001b[0;36mgenerate_prompt\u001b[0;34m(self, prompts, stop, callbacks, **kwargs)\u001b[0m\n\u001b[1;32m 1119\u001b[0m ) -> LLMResult:\n\u001b[1;32m 1120\u001b[0m \u001b[0mprompt_messages\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mto_messages\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mp\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mprompts\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1121\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgenerate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mprompt_messages\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstop\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mstop\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcallbacks\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mcallbacks\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1122\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1123\u001b[0m \u001b[0;34m@\u001b[0m\u001b[0moverride\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/langchain_core/language_models/chat_models.py\u001b[0m in \u001b[0;36mgenerate\u001b[0;34m(self, messages, stop, callbacks, tags, metadata, run_name, run_id, **kwargs)\u001b[0m\n\u001b[1;32m 929\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 930\u001b[0m results.append(\n\u001b[0;32m--> 931\u001b[0;31m self._generate_with_cache(\n\u001b[0m\u001b[1;32m 932\u001b[0m \u001b[0mm\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 933\u001b[0m \u001b[0mstop\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mstop\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/langchain_core/language_models/chat_models.py\u001b[0m in \u001b[0;36m_generate_with_cache\u001b[0;34m(self, messages, stop, run_manager, **kwargs)\u001b[0m\n\u001b[1;32m 1231\u001b[0m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mgenerate_from_stream\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0miter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mchunks\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1232\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0minspect\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msignature\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_generate\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mparameters\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"run_manager\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1233\u001b[0;31m result = self._generate(\n\u001b[0m\u001b[1;32m 1234\u001b[0m \u001b[0mmessages\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstop\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mstop\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mrun_manager\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mrun_manager\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1235\u001b[0m )\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/langchain_google_genai/chat_models.py\u001b[0m in \u001b[0;36m_generate\u001b[0;34m(self, messages, stop, run_manager, tools, functions, safety_settings, tool_config, generation_config, cached_content, tool_choice, **kwargs)\u001b[0m\n\u001b[1;32m 3049\u001b[0m )\n\u001b[1;32m 3050\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mClientError\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 3051\u001b[0;31m \u001b[0m_handle_client_error\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0me\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mrequest\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 3052\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3053\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0m_response_to_result\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mresponse\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/langchain_google_genai/chat_models.py\u001b[0m in \u001b[0;36m_handle_client_error\u001b[0;34m(e, request)\u001b[0m\n\u001b[1;32m 143\u001b[0m \u001b[0mmodel_name\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mrequest\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"model\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"unknown\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 144\u001b[0m \u001b[0mmsg\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34mf\"Error calling model '{model_name}' ({e.status}): {e}\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 145\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mChatGoogleGenerativeAIError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmsg\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 146\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 147\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mChatGoogleGenerativeAIError\u001b[0m: Error calling model 'gemini-2.5-flash' (RESOURCE_EXHAUSTED): 429 RESOURCE_EXHAUSTED. {'error': {'code': 429, 'message': 'You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/rate-limit. \\n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 20, model: gemini-2.5-flash\\nPlease retry in 18.240853364s.', 'status': 'RESOURCE_EXHAUSTED', 'details': [{'@type': 'type.googleapis.com/google.rpc.Help', 'links': [{'description': 'Learn more about Gemini API quotas', 'url': 'https://ai.google.dev/gemini-api/docs/rate-limits'}]}, {'@type': 'type.googleapis.com/google.rpc.QuotaFailure', 'violations': [{'quotaMetric': 'generativelanguage.googleapis.com/generate_content_free_tier_requests', 'quotaId': 'GenerateRequestsPerDayPerProjectPerModel-FreeTier', 'quotaDimensions': {'model': 'gemini-2.5-flash', 'location': 'global'}, 'quotaValue': '20'}]}, {'@type': 'type.googleapis.com/google.rpc.RetryInfo', 'retryDelay': '18s'}]}}" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "from IPython.display import Image, display\n", + "display(Image(app.get_graph().draw_mermaid_png()))" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 350 + }, + "id": "aW-7DdQ8qxHj", + "outputId": "a7e1cc01-b30f-4250-c375-2456f0749057" + }, + "execution_count": 21, + "outputs": [ + { + "output_type": "display_data", + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {} + } + ] + } + ] +} \ No newline at end of file diff --git a/app/ai/graph.py b/app/ai/graph.py deleted file mode 100644 index 2189fd1..0000000 --- a/app/ai/graph.py +++ /dev/null @@ -1,33 +0,0 @@ -from langgraph.graph import StateGraph, END -from nodes import * -from state import State - -workflow = StateGraph(State) - -# 添加节点 -workflow.add_node("retrieve", retrieve_node) -workflow.add_node("summarize", summarize_node) -workflow.add_node("chatbot", call_model_node) - -# 设置入口 -workflow.set_entry_point("retrieve") - -# 条件逻辑:检查消息数量 -def should_summarize(state: State): - if len(state["messages"]) > 10: - return "summarize" - return "chatbot" - -workflow.add_conditional_edges( - "retrieve", - should_summarize, - { - "summarize": "summarize", - "chatbot": "chatbot" - } -) -workflow.add_edge("summarize", "chatbot") -workflow.add_edge("chatbot", END) - -# 编译时加入 Checkpointer (你可以使用你的 Postgres 实现来持久化 State) -app = workflow.compile() #checkpointer=postgres_checkpointer \ No newline at end of file diff --git a/app/ai/nodes.py b/app/ai/nodes.py deleted file mode 100644 index 919b303..0000000 --- a/app/ai/nodes.py +++ /dev/null @@ -1,78 +0,0 @@ -# nodes/graph_nodes.py -from services.memory_service import search_memories -from services.summary_service import get_rolling_summary -from langchain_core.messages import RemoveMessage -from langchain_google_genai import ChatGoogleGenerativeAI -from langchain_core.messages import SystemMessage, HumanMessage -from state import State - -async def retrieve_node(state: State): - # 只针对最后一条用户消息进行检索 - user_query = state["messages"][-1].content - memories = await search_memories(user_query, db_connection=None) - return {"retrieved_context": memories} - -async def smart_retrieve_node(state: State): - """ - 智能检索:先判断用户是否在提问需要背景的事情 - """ - last_msg = state["messages"][-1].content - - # 一个简单的判断逻辑,也可以用 LLM 做路由 - keywords = ["之前", "记得", "上次", "习惯", "喜欢", "谁", "哪"] - if any(k in last_msg for k in keywords): - # 执行向量检索 - memories = await search_memories(last_msg) - return {"retrieved_context": memories} - - return {"retrieved_context": ""} - - -async def summarize_node(state: State): - # 设定阈值,比如保留最后 6 条,剩下的全部压缩 - THRESHOLD = 10 - if len(state["messages"]) <= THRESHOLD: - return {} - - # 取出除最后 6 条以外的消息进行压缩 - to_summarize = state["messages"][:-6] - new_summary = await get_rolling_summary(model_flash, state.get("summary", ""), to_summarize) - - # 创建 RemoveMessage 列表来清理 State - delete_actions = [RemoveMessage(id=m.id) for m in to_summarize if m.id] - - return { - "summary": new_summary, - "messages": delete_actions - } - - - -# 初始化 Gemini (确保你已经设置了 GOOGLE_API_KEY) - -llm = ChatGoogleGenerativeAI(model="gemini-1.5-pro", temperature=0.7, google_api_key=userdata.get('GOOGLE_API_KEY')) - -async def call_model_node(state: State): - """ - 这是最终生成对话的节点。 - 它负责拼接所有的上下文:Summary + Memory + Messages - """ - - # 1. 构建基础 System Prompt - system_content = "你是一个贴心的 AI 助手。" - - # 2. 注入长期摘要 (如果存在) - if state.get("summary"): - system_content += f"\n这是之前的对话简要背景:{state['summary']}" - - # 3. 注入检索到的按键记忆 (如果存在) - if state.get("retrieved_context"): - system_content += f"\n这是你记住的关于用户的重要事实:{state['retrieved_context']}" - - messages = [SystemMessage(content=system_content)] + state["messages"] - - # 4. 调用 Gemini - response = await llm.ainvoke(messages) - - # 返回更新后的消息列表 - return {"messages": [response]} \ No newline at end of file diff --git a/app/ai/services/memory_service.py b/app/ai/services/memory_service.py deleted file mode 100644 index a30eafb..0000000 --- a/app/ai/services/memory_service.py +++ /dev/null @@ -1,25 +0,0 @@ -# services/memory_service.py -from langchain_google_genai import GoogleGenerativeAIEmbeddings - -# 假设你使用的是 pgvector -async def search_memories(query: str, db_connection): - """ - 1. 将 query 转化为 Embedding - 2. 在数据库中执行向量相似度搜索 - 3. 返回最相关的 Top-K 条记忆 - """ - # 模拟实现 - embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001") - query_vector = await embeddings.aembed_query(query) - - # 这里执行 SQL: SELECT content FROM memories ORDER BY embedding <=> query_vector LIMIT 3 - results = "用户此前提到过他在做 Gemini 相关的 Hackathon,倾向于使用 Python。" - return results - -async def save_to_memory(content: str, db_connection): - """ - 这个函数由你的 '保存' 按钮触发。 - """ - # 1. 提取 content 中的关键信息(可选,可以用 LLM 提取) - # 2. 生成 Embedding 并存入数据库 - pass \ No newline at end of file diff --git a/app/ai/services/summary_service.py b/app/ai/services/summary_service.py deleted file mode 100644 index 3bacc26..0000000 --- a/app/ai/services/summary_service.py +++ /dev/null @@ -1,65 +0,0 @@ -# services/summary_service.py -from langchain_google_genai import ChatGoogleGenerativeAI -from langchain_core.messages import HumanMessage, SystemMessage - -async def get_rolling_summary(model: ChatGoogleGenerativeAI, existing_summary: str, messages: list): - """ - 将旧的总结与新的对话内容合并生成新的总结 - """ - if not messages: - return existing_summary - - msg_content = "\n".join([f"{m.type}: {m.content}" for m in messages]) - - prompt = f""" - 你是一个记忆专家。请根据提供的“现有总结”和“新增对话”,生成一个更全面、精炼的新总结。 - 请保留关键事实(如技术偏好、重要决定、用户背景),删除无意义的寒暄。 - - [现有总结]: {existing_summary if existing_summary else "暂无"} - [新增对话]: {msg_content} - - 请直接输出新的总结文本,保持中文书写。 - """ - - response = await model.ainvoke([HumanMessage(content=prompt)]) - return response.content - -# services/memory_service.py - -async def extract_and_save_fact(thread_id: str, messages: list, db_connection): - """ - 由前端按钮触发:从当前对话上下文提取事实并存入向量库 - """ - # 1. 过滤掉无意义的消息,只取最近几条作为提取素材 - context_text = "\n".join([f"{m.type}: {m.content}" for m in messages[-10:]]) - - # 2. 调用小模型 (Flash) 进行原子化事实提取 - extraction_prompt = f""" - 从以下对话中提取用户提到的、具有长期保存价值的“个人事实”或“技术偏好”。 - 要求: - - 每一条事实必须是独立的、完整的句子。 - - 不要包含寒暄或临时性的讨论。 - - 如果没有值得记录的事实,请返回 "NONE"。 - - 对话内容: - {context_text} - - 输出格式示例: - - 用户正在使用 Python 3.12 进行开发。 - - 用户计划参加 2026 年的 Gemini Hackathon。 - """ - - # 这里假设你已经初始化了 model_flash - response = await model_flash.ainvoke(extraction_prompt) - facts_text = response.content.strip() - - if facts_text == "NONE": - return "没有发现值得记录的新事实。" - - # 3. 将提取到的事实转化为向量并存入 pgvector - # facts = facts_text.split('\n') - # for fact in facts: - # embedding = await get_embedding(fact) - # await db_connection.execute("INSERT INTO memories ...", embedding, fact, thread_id) - - return f"已成功记录以下记忆:\n{facts_text}" \ No newline at end of file diff --git a/app/ai/state.py b/app/ai/state.py deleted file mode 100644 index cfe6a13..0000000 --- a/app/ai/state.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Annotated, TypedDict -from langgraph.graph.message import add_messages - -class State(TypedDict): - # add_messages 会将新消息追加到列表,而不是覆盖 - messages: Annotated[list, add_messages] - # 存储当前的总结,避免重复加载大数据量历史 - summary: str - # 从 Long-term memory 检索到的事实 - retrieved_context: str - # 记录这轮对话是否触发了总结 - retrieved_context: str \ No newline at end of file