[{"content":"个人项目文档\nRAG类项目 项目1 若干RAG小项目 项目2 RAG原理性项目 项目3 Reranker和Embedding 模型蒸馏 项目4 embedding项目微调+混合检索 项目5 function calling微调： 基于gemma3模型的function-calling微调 Agent类项目 项目6 智能客服系统 ","permalink":"https://chesterwang.github.io/chester-blog/posts/2025-11-13-%E4%B8%AA%E4%BA%BA%E5%BC%80%E5%8F%91%E9%A1%B9%E7%9B%AE%E5%B1%95%E7%A4%BA/","summary":"个人项目文档 RAG类项目 项目1 若干RAG小项目 项目2 RAG原理性项目 项目3 Reranker和Embedding 模型蒸馏 项目4 embedding项目微调+混合检索 项目5 function calling微调： 基于gemma3模型的function-calling微调 Agent类项目 项目6 智能客服系统","title":"【置顶】个人开发项目展示"},{"content":" 原始项目地址 Shubhamsaboo/awesome-llm-apps: Collection of awesome LLM apps with AI Agents and RAG using OpenAI, Anthropic, Gemini and opensource models. 实践项目地址 large_model_projects/Agent/2025-11-29-agno-demo · chesterwang/large_model_projects agno 框架 agno_hello_world.py 的role问题 (Kimi的openai接口兼容性) 原始代码为\nweb_agent = Agent( name=\u0026#34;Web Researcher\u0026#34;, model=OpenAIChat(id=\u0026#34;kimi-k2-turbo-preview\u0026#34;, base_url=OPENAI_API_BASE, api_key=OPENAI_API_KEY), tools=[DuckDuckGoTools()], instructions=[\u0026#34;Always include sources in your response.\u0026#34;], debug_mode=True, markdown=True, ) print(\u0026#34;--- Agent Running ---\u0026#34;) web_agent.print_response(\u0026#34;What are the latest key features of the Agno (Phidata) framework?\u0026#34;, stream=True) 运行报错如下 agno.exceptions.ModelProviderError: invalid request: unsupported role ROLE_UNSPECIFIED。 经过debug发现 OpenAI和KIMI 的api对于 message role的 规范有一定差异。\nOpenAI sdk中关于role的代码\n# OpenAI 的role # 参考官方文档 [Model Spec (2025/02/12)](https://model-spec.openai.com/2025-02-12.html#chain_of_command) # 代码 agno.models.openai.chat # The role to map the message role to. default_role_map = { \u0026#34;system\u0026#34;: \u0026#34;developer\u0026#34;, \u0026#34;user\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;assistant\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;tool\u0026#34;: \u0026#34;tool\u0026#34;, \u0026#34;model\u0026#34;: \u0026#34;assistant\u0026#34;, } OpenAI 官方文档中对于role的描述\nrole (required): specifies the source of each message. As described in Instructions and levels of authority and The chain of command, roles determine the authority of instructions in the case of conflicts. system: messages added by OpenAI developer: from the application developer (possibly also OpenAI) user: input from end users, or a catch-all for data we want to provide to the model assistant: sampled from the language model tool: generated by some program, such as code execution or an API call KIMI 文档中关于 role的描述（from 基本信息 - Moonshot AI 开放平台 - Kimi 大模型 API 服务）\n这是一个结构体的列表，每个元素类似如下：{\u0026ldquo;role\u0026rdquo;: \u0026ldquo;user\u0026rdquo;, \u0026ldquo;content\u0026rdquo;: \u0026ldquo;你好\u0026rdquo;} role 只支持 system,user,assistant 其一，content 不得为空 另外，对于tool_use的特性，KIMI则支持 rule=tool 的message。\n解决方案\n综上所述，临时解决方案是 将 instruction 的内容 塞到 input 变量中，即将instruction的system角色修改为 user角色。 这种方法不可行，猜测是因为即使没有增加instruction内容，在message 格式化的时候，还是会制定 developer部分内容，导致api接口报错。 最终解决方法，强行指定 role_map，将 developer 改为 system。 程序运行成功。 附注： dudugo 工具需要搜索网络，需要开启本地科学上网代理。 ","permalink":"https://chesterwang.github.io/chester-blog/posts/2025-11-29-agent%E9%A1%B9%E7%9B%AE-awesome-llm-apps-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/","summary":"原始项目地址 Shubhamsaboo/awesome-llm-apps: Collection of awesome LLM apps with AI Agents and RAG using OpenAI, Anthropic, Gemini and opensource models. 实践项目地址 large_model_projects/Agent/2025-11-29-agno-demo · chesterwang/large_model_projects agno 框架 agno_hello_world.py 的role问题 (Kimi的openai接口兼容性) 原始代码为 web_agent = Agent( name=\u0026#34;Web Researcher\u0026#34;, model=OpenAIChat(id=\u0026#34;kimi-k2-turbo-preview\u0026#34;, base_url=OPENAI_API_BASE, api_key=OPENAI_API_KEY), tools=[DuckDuckGoTools()], instructions=[\u0026#34;Always include sources in your response.\u0026#34;], debug_mode=True, markdown=True, ) print(\u0026#34;--- Agent Running ---\u0026#34;) web_agent.print_response(\u0026#34;What are the latest key features of the Agno (Phidata) framework?\u0026#34;, stream=True) 运行报错如下 agno.exceptions.ModelProviderError: invalid request: unsupported role ROLE_UNSPECIFIED。 经过debug发现 Op","title":"Agent项目 awesome-llm-apps 学习记录"},{"content":"Agent项目搭建 智能客服项目 笔记\n","permalink":"https://chesterwang.github.io/chester-blog/posts/2025-11-17-agent%E9%A1%B9%E7%9B%AE%E5%BC%80%E5%8F%91%E8%AE%B0%E5%BD%95/","summary":"Agent项目搭建 智能客服项目 笔记","title":"Agent项目开发记录"},{"content":"Function-Calling 微调 2025-11-14-Huggingface-Agents-Course 中的Function-Calling 微调 训练笔记\n原始 ipynb文件 bonus-unit1.ipynb - Colab\n模型下载 镜像站 hf-mirros.com 下载速度更快。命令如下 HF_ENDPOINT=https://hf-mirror.com huggingface-cli download google/gemma-2-2b-it 关于数据集若干问题 这里的原始数据集 只有一个split，就是train。 即类型是 DatasetDict，只有一个key=\u0026ldquo;train\u0026rdquo;。 数据下载 数据已经存在本地缓存，所以就算是执行是抛出网络错误，也不会影响加载数据集。 但会非常耗时，需要很多的网络尝试。 也可以通过设置环境变量 HF_DATASETS_OFFLINE=1 来避免网络请求。 datasetDict.map 是作用于 value的dataset类型的。 NousResearch/hermes-function-calling-v1 是指原始数据，不包含think过程。本项目中加载的数据是基于该数据集创建的新数据集，是加入了deepseek的思考过程的。 从加载的数据中，可以看出说句中有的时候开启了思考，有的时候没有开启思考，说明不同任务有不同的难度，这种数据特性使得模型也可以学习到这种能力。 训练 gemma2模型在T4GPU中无法训练，所以使用了 gemma3-1b-it 模型，使得可以在T4 GPU中运行。并且两者的 special_token尽量兼容。 那么不兼容会怎样？ 比如使用 llama类型的chat训练数据来微调gemma模型，是否会出问题？ 警告信息：The tokenizer has new PAD/BOS/EOS tokens that differ from the model config and generation config. The model config and generation config were aligned accordingly, being updated with the tokenizer's values. Updated tokens: {'eos_token_id': 1}. 其实是说 原始的模型原始的tokenizer中的special_token和 SFTTrainer中对于训练数据处理使用的 tokenizer 是不同的。 模型 model = AutoModelForCausalLM.from_pretrained(model_name, attn_implementation='eager', device_map=\u0026quot;auto\u0026quot;) 中使用的是原始模型文件中定义的tokenizer。 SFTTrainer 中加入了 \u0026lt;tools\u0026gt; \u0026lt;think\u0026gt; \u0026lt;tool_call\u0026gt; \u0026lt;tool_response\u0026gt; 但我在模型原始的tokenizer中找不到 start_of_turn end_of_turn两个特殊的token。 训练数据的消息格式非常相似，但使用该模板考虑了对训练数据中，去除了不合理的消息。具体如下 新的消息格式\n{{ bos_token }}{% if messages[0][\u0026#39;role\u0026#39;] == \u0026#39;system\u0026#39; %}{{ raise_exception(\u0026#39;System role not supported\u0026#39;) }}{% endif %}{% for message in messages %}{% if (message[\u0026#39;role\u0026#39;] == \u0026#39;user\u0026#39;) != (loop.index0 % 2 == 0) %}{{ raise_exception(\u0026#39;Conversation roles must alternate user/assistant/user/assistant/...\u0026#39;) }}{% endif %}{% if (message[\u0026#39;role\u0026#39;] == \u0026#39;assistant\u0026#39;) %}{% set role = \u0026#39;model\u0026#39; %}{% else %}{% set role = message[\u0026#39;role\u0026#39;] %}{% endif %}{{ \u0026#39;\u0026lt;start_of_turn\u0026gt;\u0026#39; + role + \u0026#39; \u0026#39; + message[\u0026#39;content\u0026#39;] | trim + \u0026#39;\u0026lt;end_of_turn\u0026gt; \u0026#39; }}{% endfor %}{% if add_generation_prompt %}{{\u0026#39;\u0026lt;start_of_turn\u0026gt;model \u0026#39;}}{% endif %} 模型原有的消息格式(gemma2)。Gemma3的消息模型更复杂，不过基本结构相同。\n{{ bos_token }}{% if messages[0][\u0026#39;role\u0026#39;] == \u0026#39;system\u0026#39; %}{{ raise_exception(\u0026#39;System role not supported\u0026#39;) }}{% endif %}{% for message in messages %}{{ \u0026#39;\u0026lt;start_of_turn\u0026gt;\u0026#39; + message[\u0026#39;role\u0026#39;] + \u0026#39;\\n\u0026#39; + message[\u0026#39;content\u0026#39;] | trim + \u0026#39;\u0026lt;end_of_turn\u0026gt;\u0026lt;eos\u0026gt;\\n\u0026#39; }}{% endfor %}{% if add_generation_prompt %}{{\u0026#39;\u0026lt;start_of_turn\u0026gt;model\\n\u0026#39;}}{% endif %} 训练数据结构说明\n关键是其中的tools和tool_call 部分。\n数据来源 https://huggingface.co/datasets/Jofthomas/hermes-function-calling-thinking-V1\n[ { \u0026#34;content\u0026#34;: \u0026#34;You are a function calling AI model. You are provided with function signatures within \u0026lt;tools\u0026gt;\u0026lt;/tools\u0026gt; XML tags.You may call one or more functions to assist with the user query. Don\u0026#39;t make assumptions about what values to plug into functions.Here are the available tools: \u0026lt;tools\u0026gt; [ { \u0026#39;type\u0026#39;: \u0026#39;function\u0026#39;, \u0026#39;function\u0026#39;: { \u0026#39;name\u0026#39;: \u0026#39;get_stock_price\u0026#39;, \u0026#39;description\u0026#39;: \u0026#39;Get the current stock price of a company\u0026#39;, \u0026#39;parameters\u0026#39;: { \u0026#39;type\u0026#39;: \u0026#39;object\u0026#39;, \u0026#39;properties\u0026#39;: { \u0026#39;company\u0026#39;: { \u0026#39;type\u0026#39;: \u0026#39;string\u0026#39;, \u0026#39;description\u0026#39;: \u0026#39;The name of the company\u0026#39; } }, \u0026#39;required\u0026#39;: [\u0026#39;company\u0026#39;] } } }, { \u0026#39;type\u0026#39;: \u0026#39;function\u0026#39;, \u0026#39;function\u0026#39;: { \u0026#39;name\u0026#39;: \u0026#39;get_movie_details\u0026#39;, \u0026#39;description\u0026#39;: \u0026#39;Get details about a movie\u0026#39;, \u0026#39;parameters\u0026#39;: { \u0026#39;type\u0026#39;: \u0026#39;object\u0026#39;, \u0026#39;properties\u0026#39;: { \u0026#39;title\u0026#39;: { \u0026#39;type\u0026#39;: \u0026#39;string\u0026#39;, \u0026#39;description\u0026#39;: \u0026#39;The title of the movie\u0026#39; } }, \u0026#39;required\u0026#39;: [\u0026#39;title\u0026#39;] } } } ] \u0026lt;/tools\u0026gt; Use the following pydantic model json schema for each tool call you will make: {\u0026#39;title\u0026#39;: \u0026#39;FunctionCall\u0026#39;, \u0026#39;type\u0026#39;: \u0026#39;object\u0026#39;, \u0026#39;properties\u0026#39;: { \u0026#39;arguments\u0026#39;: {\u0026#39;title\u0026#39;: \u0026#39;Arguments\u0026#39;, \u0026#39;type\u0026#39;: \u0026#39;object\u0026#39;}, \u0026#39;name\u0026#39;: {\u0026#39;title\u0026#39;: \u0026#39;Name\u0026#39;, \u0026#39;type\u0026#39;: \u0026#39;string\u0026#39;} }, \u0026#39;required\u0026#39;: [\u0026#39;arguments\u0026#39;, \u0026#39;name\u0026#39;] } For each function call return a json object with function name and arguments within \u0026lt;tool_call\u0026gt;\u0026lt;/tool_call\u0026gt; XML tags as follows:\\n\u0026lt;tool_call\u0026gt;\\n{tool_call}\\n\u0026lt;/tool_call\u0026gt;\u0026#34;, \u0026#34;role\u0026#34;: \u0026#34;system\u0026#34; }, { \u0026#34;content\u0026#34;: \u0026#34;Hi, can you tell me the current stock price of Apple?\u0026#34;, \u0026#34;role\u0026#34;: \u0026#34;human\u0026#34; }, { \u0026#34;content\u0026#34;: \u0026#34;\u0026lt;think\u0026gt;Alright, the user asked for the current stock price of Apple. Looking at the tools available, there\u0026#39;s a function called get_stock_price which exactly fits this request. I\u0026#39;ll need to call this function with the parameter company set to \u0026#39;Apple\u0026#39; to retrieve the information the user is asking for. This seems like the most straightforward and appropriate action to take.\\n\u0026lt;/think\u0026gt; \u0026lt;tool_call\u0026gt;\\n{\u0026#39;name\u0026#39;: \u0026#39;get_stock_price\u0026#39;, \u0026#39;arguments\u0026#39;: {\u0026#39;company\u0026#39;: \u0026#39;Apple\u0026#39;}}\\n\u0026lt;/tool_call\u0026gt;\u0026#34;, \u0026#34;role\u0026#34;: \u0026#34;model\u0026#34; }, { \u0026#34;content\u0026#34;: \u0026#34;\u0026lt;tool_response\u0026gt;\\n{\u0026#39;stock_price\u0026#39;: \u0026#39;$150.75\u0026#39;}\\n\u0026lt;/tool_response\u0026gt;\u0026#34;, \u0026#34;role\u0026#34;: \u0026#34;tool\u0026#34; }, { \u0026#34;content\u0026#34;: \u0026#34;The current stock price of Apple is $150.75.\u0026#34;, \u0026#34;role\u0026#34;: \u0026#34;model\u0026#34; }, { \u0026#34;content\u0026#34;: \u0026#34;That\u0026#39;s great. Can you also tell me the stock price for Microsoft?\u0026#34;, \u0026#34;role\u0026#34;: \u0026#34;human\u0026#34; }, { \u0026#34;content\u0026#34;: \u0026#34;\u0026lt;think\u0026gt;Alright, let me try to figure out how to handle this user\u0026#39;s request. The user first asked about Apple\u0026#39;s stock price, and I responded by calling the get_stock_price function with \u0026#39;Apple\u0026#39; as the argument. The tool provided the price, and I passed that information back to the user.\\n\\nNow, the user is asking for the stock price of Microsoft. I need to determine the appropriate action here. Looking at the tools available, there\u0026#39;s still the get_stock_price function, which is designed to fetch current stock prices. Since the user is requesting another company\u0026#39;s stock price, it makes sense to use the same function again but with the company name changed to Microsoft. \\n\\nI shouldn\u0026#39;t assume any other tools are needed because the available functions are specifically tailored for stock prices and movie details. In this case, the relevant function is get_stock_price. I should ensure that the function is called with the correct parameter, \u0026#39;Microsoft\u0026#39; in this case, to retrieve the accurate information the user is seeking. This approach is efficient and aligns directly with the user\u0026#39;s request without making unnecessary assumptions or using unrelated tools.\\n\u0026lt;/think\u0026gt; \u0026lt;tool_call\u0026gt;\\n{\u0026#39;name\u0026#39;: \u0026#39;get_stock_price\u0026#39;, \u0026#39;arguments\u0026#39;: {\u0026#39;company\u0026#39;: \u0026#39;Microsoft\u0026#39;}}\\n\u0026lt;/tool_call\u0026gt;\u0026#34;, \u0026#34;role\u0026#34;: \u0026#34;model\u0026#34; }, { \u0026#34;content\u0026#34;: \u0026#34;\u0026lt;tool_response\u0026gt;\\n{\u0026#39;stock_price\u0026#39;: \u0026#39;$210.22\u0026#39;}\\n\u0026lt;/tool_response\u0026gt;\u0026#34;, \u0026#34;role\u0026#34;: \u0026#34;tool\u0026#34; }, { \u0026#34;content\u0026#34;: \u0026#34;The current stock price of Microsoft is $210.22.\u0026#34;, \u0026#34;role\u0026#34;: \u0026#34;model\u0026#34; }, { \u0026#34;content\u0026#34;: \u0026#34;Thank you for the information.\u0026#34;, \u0026#34;role\u0026#34;: \u0026#34;human\u0026#34; }, { \u0026#34;content\u0026#34;: \u0026#34;You\u0026#39;re welcome! If you have any other questions, feel free to ask.\u0026#34;, \u0026#34;role\u0026#34;: \u0026#34;model\u0026#34; } ] ","permalink":"https://chesterwang.github.io/chester-blog/posts/2025-11-14-llm%E8%AE%AD%E7%BB%83%E5%BE%AE%E8%B0%83%E5%BC%80%E5%8F%91%E8%AE%B0%E5%BD%95/","summary":"Function-Calling 微调 2025-11-14-Huggingface-Agents-Course 中的Function-Calling 微调 训练笔记 原始 ipynb文件 bonus-unit1.ipynb - Colab 模型下载 镜像站 hf-mirros.com 下载速度更快。命令如下 HF_ENDPOINT=https://hf-mirror.com huggingface-cli download google/gemma-2-2b-it 关于数据集若干问题 这里的原始数据集 只有一个split，就是train。 即类型是 DatasetDict，只有一个key=\u0026ldquo;train\u0026rdquo","title":"LLM训练微调开发记录"},{"content":"项目环境搭建 如何将requirements.txt中的依赖转移到 uv环境中 uv add -r requirements.txt 如果太慢则加 url uv add -r requirements.txt --index-url https://pypi.tuna.tsinghua.edu.cn/simple ollama搭建 报错 ollama修改模型下载目录之后报错 error: pull model manifest: 503: no healthy upstream 重启服务即可 sudo systemctl stop ollama sudo systemctl start ollama 修改模型目录 sudo ln -s /home/chester/ollama/models /usr/share/ollama/.ollama/ sudo chown ollama /usr/share/ollama/.ollama/models sudo chgrp ollama /usr/share/ollama/.ollama/models 后两条是保证目录的权限问题，不然ollma运行会报错 资料来源 Does anyone know how to change where your models are saved on linux? : r/ollama Ollama(model=\u0026ldquo;qwen3:0.6b\u0026rdquo;) 这段代码前提是 系统中 后台启动了ollama服务（linux安装之后会自动加入系统服务，无需手动启动该服务），但并不需要 命令行执行 ollama run model。应该是后台会自动吊起对应的模型进行计算并传回python。 本地运行ollama真的太慢了。 embedding推理服务方法 HuggingFaceBgeEmbeddings 在pycharm中运行的时候，要给jupyter server 添加参数，配上http_proxy的代理。在pycharm软件层面设置http_proxy是没用的。 huggingface会下载模型建立本地服务进行计算。 HuggingFaceInferenceAPIEmbeddings 是通过api进行embedding计算，而不是本地进行计算。 问题：httpcore 一直报错没有安装 最后发现是httpcore没有安装好，对应的python包只有元数据没有真实代码，将对应目录删除后重刷uv环境即可。 P01 RAG-Projects 以下为小的RAG子项目\nProject 01 chatbot 问题：load_dotenv的机制\nload_dotenv 定义如下 默认加载 .env文件，如果当前目录不存在，就不断提升目录层次来查找。 interpolate 用于是否解析字符串中的变量，比如 PATH=${PATH}:/usr/share/bin ，就会把PATH变量进行解析替换。 其中 dotenv.main.resolve_variables 对于interpolate 功能的实现很有意思，使用了正则方法来匹配并分别进行替换。 def load_dotenv( dotenv_path: Optional[StrPath] = None, stream: Optional[IO[str]] = None, verbose: bool = False, override: bool = False, interpolate: bool = True, encoding: Optional[str] = \u0026#34;utf-8\u0026#34;, ) -\u0026gt; bool: \u0026#34;\u0026#34;\u0026#34;Parse a .env file and then load all the variables found as environment variables. Parameters: dotenv_path: Absolute or relative path to .env file. stream: Text stream (such as `io.StringIO`) with .env content, used if `dotenv_path` is `None`. verbose: Whether to output a warning the .env file is missing. override: Whether to override the system environment variables with the variables from the `.env` file. encoding: Encoding to be used to read the file. Returns: Bool: True if at least one environment variable is set else False If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the .env file with it\u0026#39;s default parameters. If you need to change the default parameters of `find_dotenv()`, you can explicitly call `find_dotenv()` and pass the result to this function as `dotenv_path`. If the environment variable `PYTHON_DOTENV_DISABLED` is set to a truthy value, .env loading is disabled. \u0026#34;\u0026#34;\u0026#34; Project 02 APIs ChatMessagePromptTemplate 和 ChatPromptTemplate 是两个不同的类，主要区别如下： ChatMessagePromptTemplate 用于表示单个聊天消息的模板 对应单条消息，包含消息的角色（如系统、用户、助手）和内容模板 是构成聊天模板的基本单元 ChatPromptTemplate 用于表示整个聊天对话的模板 可以包含多个 ChatMessagePromptTemplate 实例 用于构建完整的对话历史模板，支持多轮对话场景 简而言之，ChatMessagePromptTemplate 是单条消息模板，而 ChatPromptTemplate 是整个对话模板，后者可以包含多个前者来构建完整的对话上下文。 ChatPromptTemplate from_messages 从若干message中构造模板 from_template 从单个 message 的 template 来构造 HumanMessage，并组装为一个Chat。 FewShotChatMessagePromptTemplate langchain_core.prompts中的各种prompt。 ChatOpenAI的使用方法 langchain_openai.chat_models.base.ChatOpenAI — 🦜🔗 LangChain 0.2.17 StrOutputParser 如何解析chain之前部分的内容 langchain_core.outputs.chat_generation.ChatGeneration.set_text 中指明了从message中的content属性来获取内容。 Project 04 Retriever and Chain create_stuff_documents_chain RunnablePassthrough 即 identity function，恒等函数，输入什么就输出什么。 RunnablePassthrough.assign ： Merge the Dict input with the output produced by the mapping argument. chain的调用过程 输入：输入数据为dict，有input这个key。 retrieval_docs ：先对 数据进行检索，即从x中获取 input这一项，然后送入检索器。检索器本质上是调用了 __ror__方法，进一步调用了 VectorStoreRetriever的 _get_relevant_documents 方法，进一步调用了 self.vectorstore.similarity_search方法。检索完之后，将相似文档组装为一个item放入到 context这个key下。 数据变为 {input:XXX, context:[docs]}。 format_docs ：检索出相似文档之后，送入format_inputs过程，即将数据中的context项格式为字符串，并用 双换行符进行分隔连接。数据变为 {input:XXX, context:doc_str} prompt： 送入prompt，prompt的invoke方法中本质就是对 template字符串和 上一步计算出的数据进行格式化。 llm：模型给出响应 _output_parser 本质就是对 输出的response消息进行格式化，本质就是提取消息中的key为\u0026quot;text\u0026quot;对应的属性。 相关关键代码如下\nresponse=retriever.invoke(input=\u0026#34;\u0026#34;\u0026#34;image processing with an introduction to techniques for image pattern classification\u0026#34;\u0026#34;\u0026#34;) #langchain_classic/chains/retrieval.py:64 if not isinstance(retriever, BaseRetriever): retrieval_docs: Runnable[dict, RetrieverOutput] = retriever else: retrieval_docs = (lambda x: x[\u0026#34;input\u0026#34;]) | retriever return ( RunnablePassthrough.assign( context=retrieval_docs.with_config(run_name=\u0026#34;retrieve_documents\u0026#34;), ).assign(answer=combine_docs_chain) ).with_config(run_name=\u0026#34;retrieval_chain\u0026#34;) def format_docs(inputs: dict) -\u0026gt; str: return document_separator.join( format_document(doc, _document_prompt) for doc in inputs[document_variable_name] ) return ( RunnablePassthrough.assign(**{document_variable_name: format_docs}).with_config( run_name=\u0026#34;format_inputs\u0026#34;, ) | prompt | llm | _output_parser ).with_config(run_name=\u0026#34;stuff_documents_chain\u0026#34;) Project 05 Advanced RAG Q\u0026amp;A Project create_retriever_tool 本质就是 将检索、格式化、拼接工作组装为一个整体功能然后，实例化为一个tool。 Scratchpad 含义 直译为草稿纸 在 LangChain 或类似框架中，scratchpad 通常指代理(Agent)的临时工作空间，用于： 存储中间思考过程 记录当前任务状态 保存临时计算结果 进行推理和决策的缓冲区 这是一个通用的计算机科学概念，表示任何用于临时存储和处理的内存区域。 最终tools是在 ollama._types.ChatRequest 进行解析，具体见下面代码。 这里的llm走的是chat类型的llm，所以底层是走 /api/chat 接口，该接口的参数列表中含有tools。相当于该接口已经把tools的描述如何格式化都已经在内部解决掉，不需要调用方将tools的描述格式化到 prompt里面。 AgentExecutor里最重视依赖 iter方法来不断迭代，底层是依赖_iter_next_step来进行 thought、action步骤的。 即chat_model的接口都是支持在请求中加入tools参数的。 prompt hub https://smith.langchain.com/hub/hwchase17/openai-functions-agent 函数调用 - OpenAI API \u0026mdash; Function calling - OpenAI API 相关关键代码如下\n#Runnable的方法 def bind(self, **kwargs: Any) -\u0026gt; Runnable[Input, Output]: \u0026#34;\u0026#34;\u0026#34;Bind arguments to a `Runnable`, returning a new `Runnable`. Useful when a `Runnable` in a chain requires an argument that is not in the output of the previous `Runnable` or included in the user input. \u0026#34;\u0026#34;\u0026#34; #langchain_ollama.chat_models.ChatOllama._create_chat_stream # ollama/_client.py:351 # ollama._client.Client.chat return self._request( ChatResponse, \u0026#39;POST\u0026#39;, \u0026#39;/api/chat\u0026#39;, json=ChatRequest( model=model, messages=list(_copy_messages(messages)), tools=list(_copy_tools(tools)), stream=stream, think=think, format=format, options=options, keep_alive=keep_alive, ).model_dump(exclude_none=True), stream=stream, ) #langchain_classic.agents.agent.AgentExecutor._iter_next_step def _iter_next_step( self, name_to_tool_map: dict[str, BaseTool], color_mapping: dict[str, str], inputs: dict[str, str], intermediate_steps: list[tuple[AgentAction, str]], run_manager: CallbackManagerForChainRun | None = None, ) -\u0026gt; Iterator[AgentFinish | AgentAction | AgentStep]: \u0026#34;\u0026#34;\u0026#34;Take a single step in the thought-action-observation loop. Override this to take control of how the agent makes and acts on choices. \u0026#34;\u0026#34;\u0026#34; try: intermediate_steps = self._prepare_intermediate_steps(intermediate_steps) # Call the LLM to see what to do. output = self._action_agent.plan( intermediate_steps, callbacks=run_manager.get_child() if run_manager else None, **inputs, ) except OutputParserException as e: ...... # If the tool chosen is the finishing tool, then we end and return. if isinstance(output, AgentFinish): yield output return actions: list[AgentAction] actions = [output] if isinstance(output, AgentAction) else output for agent_action in actions: yield agent_action for agent_action in actions: yield self._perform_agent_action( name_to_tool_map, color_mapping, agent_action, run_manager, ) ... Project 06 Groq inference 本地服务和远程服务的代理设置，如下面代码 HTTP_PROXY 和 HTTPS_PROXY用于设置通过http服务的代理 NO_PROXY 用于本地服务绕过上面设置的代理，如本地的Ollama服务。 vectorstore.as_retriever 可以直接转换为检索器 方法参数search_kwargs 中也可以设置返回的文档数目k、MMR多样性算法、元数据过滤器函数等。 import os # 设置http的代理和不代理的地址 os.environ[\u0026#39;HTTP_PROXY\u0026#39;] = \u0026#34;http://127.0.0.1:7890\u0026#34; os.environ[\u0026#39;HTTPS_PROXY\u0026#39;] = \u0026#34;http://127.0.0.1:7890\u0026#34; os.environ[\u0026#39;NO_PROXY\u0026#39;] = \u0026#34;http://127.0.0.1:11434\u0026#34; #ollama的本地服务地址 Project 07 Gen AI HuggingFaceEndpoint 接口问题 bug https://github.com/langchain-ai/langchain/issues/31434#issuecomment-2936308959 因为官方服务不再提供 text-generation的模型服务。 可以使用 ChatHuggingFace来绕过这个问题。 Project 08 Powerful Doc Q\u0026amp;A Chatbot OllamaEmbeddings 在本地的笔记本上对于4篇论文的计算非常慢。 使用了HuggingFaceEmbeddings加一个小emb模型计算时间变少了很多。 Project 09 Advance Q\u0026amp;A Chatbot langchain-ObjectBox 版本太老，没法安装。该数据库 ObjectBox 维护似乎不是很多。 Project 11 ImageEnhancer model_scope 兼容接口 API推理介绍 · 文档中心 兼容openai接口 streamlit 应用 debug 选择module streamlit 参数填写 run app.py working directory 填写app.py的目录 使用modelscope服务来代替openai的图像生成接口。 文生图模型 支持API调用的模型列表，可以通过AIGC模型页面进行搜索。 上述页面中列出的很多模型都微调的很差，使用后生成结果很多并不能遵从指令。 调整了 gpu_memory_utilization 从0.9 到0.95 报错 ValueError: To serve at least one request with the models's max seq len (40960), (5.62 GiB KV cache is needed, which is larger than the available KV cache memory (4.81 GiB). 差一点内存，所以增加了gpu的利用率。 从这点看，大模型一次推理非常消耗显存。 一条数据的最大显存消耗 显存消耗=2(key+value) * seq_len(40960) * model_layer * hidden_size * head_dim * 2(byte FP16) P02 demo_simple_rag_py 该项目展示RAG原理的极简代码。\n参考资料 关联博文 Code a simple RAG from scratch 项目代码 demo.py · ngxson/demo_simple_rag_py at main 很好的展示RAG原理性的小demo，不需要安装向量数据库。 模型ID bartowski/Llama-3.2-1B-Instruct-GGUF 这个模型中的instruct的意思 instruct意思就是指模型是经过 在base model上经过指令微调（或者叫监督微调）过程之后产生的模型，可以直接用于聊天、助手类应用 Ollama问题 模型下载问题 ollama 拉取hf.co 网站的模型失败 Ollama pull 进度已经100%， 但为什么会报如下错误 Error: max retries exceeded: Get \u0026quot;https://huggingface.co/v2/bartowski/Llama-3.2-1B-Instruct-GGUF/blobs/sha256:948af2743fc78a328dcb3b0f5a31b3d75f415840fdb699e8b1235978392ecf85?__sign=eyJhbGciOiJFZERTQSJ9.eyJyZWFkIjp0cnVlLCJwZXJtaXNzaW9ucyI6eyJyZXBvLmNvbnRlbnQucmVhZCI6dHJ1ZX0sImlhdCI6MTc2Mjc1MzA3Niwic3ViIjoiL2JhcnRvd3NraS9MbGFtYS0zLjItMUItSW5zdHJ1Y3QtR0dVRiIsImV4cCI6MTc2Mjc1MzY3NiwiaXNzIjoiaHR0cHM6Ly9odWdnaW5nZmFjZS5jbyJ9.wVhqKCuc0vtHoS8DUStd86RoS1dhE0e-IqqMIpLZ_mEbfz6ahWeaoQiuTpWuaJCels7Q7uIIwEky9DhHsynYCg\u0026quot;: dial tcp 54.89.135.129:443: connect: connection refused 检查网络是可以连接 hf.co网站的。 找不到原因。 ollama 也可以在本地对模型进行量化。 quantizing-a-model - Ollama ollama对仓库模型的选择 ollama run model_repo_id，如果repo中有多版本模型，那么ollama会选择Q4版本的。更好的方法是显式指定具体版本 ollama run hf.co/{username}/{repository}:{quantization} 比如 ollama run hf.co/bartowski/Llama-3.2-3B-Instruct-GGUF:Llama-3.2-3B-Instruct-IQ3_M.gguf 参考资料 Use Ollama with any GGUF Model on Hugging Face Hub 模型文件问题 GGUF 文件包含了tokenizer的数据。 模型的Ollama构建方法：手动下载gguf模型文件，然后本地进行ollama手动构建。 手动下载 https://huggingface.co/bartowski/Llama-3.2-1B-Instruct-GGUF/tree/main 中的 Llama-3.2-1B-Instruct-Q4_K_M.gguf文件。 然后根据文档Importing a Model - Ollama来导入到本地ollama的缓存目录。 创建一个临时空目录 创建 ModelFile文件，内容为 FROM ./Llama-3.2-1B-Instruct-Q4_K_M.gguf 执行命令ollama create bartowski/Llama-3.2-1B-Instruct-GGUF 模型就可以被构建缓并复制到 ollama的缓存目录中。 并同样构建 bge-base-en-v1.5-q4_k_m.gguf 模型 ollama.embed(model=\u0026quot;CompendiumLabs/bge-base-en-v1.5-gguf\u0026quot;) 这里的模型名按照/来分割，即说明是本地文件的模型。 如果是正常的:分割的模型名，则ollama会自动从缓存目录中的 library子目录中查找。 在ChatModel的stream模式下，返回是一个词一个词的进行返回的。 在该demo示例中，检索到的文档是全被塞到一个system message下，然后后续的对话但就是根据此来进行交互。 P03 RAG-Optimization-Practices 项目原理和结果 相关资料 项目来源 kanhaoning/RAG-Optimization-Practices Quickstart — Sentence Transformers documentation 官方文档很不错。 阿里云服务器 uv 下载经常超时的解决方案，非常好用。 export UV_HTTP_TIMEOUT=240 export UV_CONCURRENT_DOWNLOADS=2 如何加快安装uv环境、缓存 在一台便宜的机器上使用uv来构建一个dummy项目，uv的缓存就会留下。 rsync uv缓存目录到 目标服务器的uv缓存目录下。同一个vpc下两个服务器的rsync速度非常快，300M/s。并且rsync会保持文件的软链接。 这样整体花钱少，并且构建速度快。 Qwen-reranker和bert一样是双向注意力计算方式。 对于一个batch内的数据，有很多条数据有共同的前缀，那么vLLM中有针对这种情况的优化推理策略 阿里云ossfs的代码存档。 安装ossfs wget https://github.com/aliyun/ossfs/releases/download/v1.91.8/ossfs_1.91.8_ubuntu24.04_amd64.deb sudo apt install ./ossfs_1.91.8_ubuntu24.04_amd64.deb 环境配置 A10 GPU（显存24G） Reranker distillation整体步骤 说明 模型本质就是cross-encoder，即将QA进行拼接，输入LLM进而得到相似度。 generate_logits 生成59万条数据（训练集+测试集），运行时长（250min）。 即针对 query 和doc 生成一个logprob分数，即 (query, passage, score)。 logprob分数利用了 prompt模板 + LLM的chat接口 进行打分。 prompt模板如下。 Judge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be \u0026quot;yes\u0026quot; or \u0026quot;no\u0026quot;. \u0026lt;Instruct\u0026gt;: {instruction}\\n\u0026lt;Query\u0026gt;: {query}\\n\u0026lt;Document\u0026gt;: {doc} 其中 instruct变量为 Given a web search query, retrieve relevant passages that answer the query prompt 限制了输出token的范围为 两个token （yes or no） 。 推理使用 vLLM的sdk，加载本地模型文件。 三元组蒸馏数据（训练集+测试集） 生成三元组蒸馏数据 (query, positive passage, negative passage, logits_diff) 正样本为logits 最高top_k里抽样。 负样本按照logits排序 每个正样本位置后面的样本。 即两两组合，组合数量为 n*(n-1)/2 并非使用该项目数据集真正的原始 ground-truth label。 训练 利用 embedding小模型 + 三元组蒸馏数据 进行训练，将领域知识蒸馏进小模型。 评估 对微调前的模型和微调后的模型利用测试集数据进行对比。 Embedding-distillatioin整体步骤 说明 模型其实就是bi-encoder，即模型输出embedding，然后计算QA相似度。 用KL散度将Qwen3-Embedding-8B向量大模型知识蒸馏给小模型BGE-m3 - 知乎 generate_logits 针对原始数据中每一行中的 一个query、多个positive文档、多个negative文档 分别输入 Qwen3-embedding模型，得到向量。 针对数据形式 （query，positiveDoc，[negativeDoc,negativeDoc2，...]），分别计算query和每个doc的余弦相似度，然后将 所有的相似度 视为一个概率分布。 train.sh 对于学生模型同样产生一个概率分布，和老师模型的分布计算KL损失，进而训练学生模型。 评估 利用预留的测试集进行评估。 uv 缓存管理问题 因为 项目目录和 uv缓存目录不在同一个文件系统下，所以uv缓存无法高效运转，只能只用硬复制来构建venv环境。所以需要迁移uv缓存到同一个文件系统下。 清理缓存项目，uv管理。 du -sh ~/.cache/uv .venv 可以用来统计 venv是否真正多占用了存储。 uv 缓存迁移的时候 不能使用 mv 和 cp命令，必须使用 rsync -av 命令，不然无法保持 软硬链接的关系。 uv 缓存 通过 .bashrc 环境变量来指定 export UV_CACHE_DIR=/mnt/fast-ssd/uv-cache 上述完成之后，项目的uv构建速度大幅提升至\u0026lt;1秒。 CrossEncoder Cross-Encoders — Sentence Transformers documentation Cross-Encoder achieve higher performance than Bi-Encoders, however, they do not scale well for large datasets. Here, it can make sense to combine Cross- and Bi-Encoders, for example in Information Retrieval / Semantic Search scenarios: First, you use an efficient Bi-Encoder to retrieve e.g. the top-100 most similar sentences for a query. Then, you use a Cross-Encoder to re-rank these 100 hits by computing the score for every (query, hit) combination. prompt格式\n# prompt格式 message = [ {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;Judge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be \\\u0026#34;yes\\\u0026#34; or \\\u0026#34;no\\\u0026#34;.\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: f\u0026#34;\u0026lt;Instruct\u0026gt;: {instruction}\\n\u0026lt;Query\u0026gt;: {query}\\n\u0026lt;Document\u0026gt;: {doc}\u0026#34;} ] #LLM采样参数，只允许 yes 和 no 选项的logpro输出。 # 定义固定的 token 和采样参数 true_token = tokenizer(\u0026#34;yes\u0026#34;, add_special_tokens=False).input_ids[0] false_token = tokenizer(\u0026#34;no\u0026#34;, add_special_tokens=False).input_ids[0] sampling_params = SamplingParams( temperature=0, max_tokens=1, logprobs=20, allowed_token_ids=[true_token, false_token], ) reranker蒸馏评估结果\n# reranker 蒸馏训练之后的评估结果 (rag-optimization-practices) root@dsw-1457330-845c588466-dwlc9:/mnt/workspace/large_model_demo/RAG/RAG-Level-02/P03-RAG-Optimization-Practices/Reranker-Distillation# bash evaluate.sh 正在从 data/test.jsonl 加载数据集... 加载完成！共 2992 条样本。 --- 正在加载并评估模型: /mnt/workspace/modelscope/BAAI/bge-reranker-v2-m3 --- --- 正在加载并评估模型: ./output/checkpoint-1217 --- ================================================== ✅ 最终评估结果汇总 ================================================== 【蒸馏前】模型性能: - MAP: 0.472061 - MRR@10: 0.478234 - NDCG@10: 0.547284 【蒸馏后】模型性能: - MAP: 0.564523 - MRR@10: 0.573106 - NDCG@10: 0.638634 ================================================== 🚀 性能变化分析 (蒸馏后 vs. 蒸馏前) ================================================== 指标 [MAP]: - 绝对提升: +0.092462 - 相对提升: +19.59% ↑ 指标 [MRR@10]: - 绝对提升: +0.094872 - 相对提升: +19.84% ↑ 指标 [NDCG@10]: - 绝对提升: +0.091349 - 相对提升: +16.69% ↑ 评估完成！✨ embedding蒸馏评估结果\n(rag-optimization-practices) root@dsw-1457330-845c588466-dwlc9:/mnt/workspace/large_model_demo/RAG/RAG-Level-02/P03-RAG-Optimization-Practices/Embedding-Distillation# bash evaluation.sh ============================================================ 模型性能对比评估 ============================================================ ------------------------------ 领域内数据集评估 ------------------------------ 正在加载模型: /mnt/workspace/modelscope/BAAI/bge-m3 正在加载数据集: data/dataset_scidocs/test.jsonl 成功加载 3978 条样本 开始评估... Batches: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 63/63 [00:06\u0026lt;00:00, 9.34it/s] Batches: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1854/1854 [03:29\u0026lt;00:00, 8.85it/s] 正在加载模型: output/checkpoint-477 正在加载数据集: data/dataset_scidocs/test.jsonl 成功加载 3978 条样本 开始评估... Batches: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 63/63 [00:06\u0026lt;00:00, 9.28it/s] Batches: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1854/1854 [03:34\u0026lt;00:00, 8.63it/s] ------------------------------ 领域外数据集评估 ------------------------------ 正在加载模型: /mnt/workspace/modelscope/BAAI/bge-m3 正在加载数据集: data/dataset_stackoverflowdupquestions/test.jsonl 成功加载 2992 条样本 开始评估... Batches: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 47/47 [00:04\u0026lt;00:00, 10.51it/s] Batches: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1398/1398 [01:56\u0026lt;00:00, 12.03it/s] 正在加载模型: output/checkpoint-477 正在加载数据集: data/dataset_stackoverflowdupquestions/test.jsonl 成功加载 2992 条样本 开始评估... Batches: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 47/47 [00:04\u0026lt;00:00, 10.15it/s] Batches: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1398/1398 [01:57\u0026lt;00:00, 11.88it/s] ============================================================ 领域内数据集 性能对比 ============================================================ 指标 蒸馏前 蒸馏后 绝对变化 相对变化(%) ------------------------------------------------------------ map 0.7744 0.8516 +0.0773 +9.98 mrr@10 0.9321 0.9548 +0.0227 +2.43 ndcg@10 0.8296 0.8956 +0.0660 +7.96 ============================================================ 领域外数据集 性能对比 ============================================================ 指标 蒸馏前 蒸馏后 绝对变化 相对变化(%) ------------------------------------------------------------ map 0.5166 0.5034 -0.0132 -2.56 mrr@10 0.5238 0.5110 -0.0127 -2.43 ndcg@10 0.5902 0.5778 -0.0124 -2.10 评估完成 问题：对于一个batch内的数据，有很多条数据有共同的前缀，那么vLLM中有针对这种情况的优化推理策略 是的！vLLM 确实针对「batch 内多条请求具有共同前缀（shared prefix）」的场景提供了专门的优化策略，这项技术称为：\n✅ Prefix Caching（前缀缓存） 或 Prompt Sharing 这是 vLLM 的核心优化之一，能显著节省显存、提升吞吐、降低延迟，尤其适用于以下场景：\nRAG（检索增强生成）：多个文档共享相同的 prompt 模板 多轮对话系统：历史上下文相同，仅用户最新输入不同 批量推理：如 “总结以下文本：\u0026lt;text\u0026gt;”，其中 \u0026lt;text\u0026gt; 不同但指令相同 🔍 工作原理\n1. KV Cache 共享\nLLM 推理中，Prefill 阶段会为 prompt 生成 KV Cache（Key/Value 缓存） 如果多个请求的 prompt 前缀完全相同，它们的 KV Cache 前缀部分也完全相同 vLLM 会： 只计算一次该公共前缀的 KV Cache 多个请求共享这份缓存 后续各自不同的部分独立计算 📌 本质：避免重复计算和存储相同的上下文\n2. 与 PagedAttention 协同\nvLLM 使用 PagedAttention 管理 KV Cache（类似操作系统的虚拟内存分页） 公共前缀的 KV Cache 被存储在共享物理页中 每个请求的逻辑块（logical blocks）指向这些共享页 + 自有页 Request 1: [Shared Prefix] + [Suffix A] Request 2: [Shared Prefix] + [Suffix B] Request 3: [Shared Prefix] + [Suffix C] → KV Cache: Shared Pages: [K/V of \u0026#34;Shared Prefix\u0026#34;] Private Pages: [K/V of \u0026#34;Suffix A\u0026#34;], [Suffix B], [Suffix C] 🚀 性能收益\n指标 优化效果 显存占用 ↓ 显著减少（公共部分只存一份） Prefill 计算量 ↓ 减少（公共前缀只计算一次） 吞吐（throughput） ↑ 提升 2–5 倍（实测常见） 支持 batch size ↑ 可容纳更多请求 💡 实测案例（RAG 场景）：\n10 个请求，共享 2K token 的 prompt 模板 显存节省 \u0026gt;60%，吞吐提升 3.2 倍 🛠 如何启用？（vLLM 中默认开启！）\n无需额外配置：vLLM 默认自动检测并启用 Prefix Caching 条件： 多个请求的 prompt 前缀必须字节级完全相同 使用相同的模型和参数（temperature 等不影响） 示例（Python client）：\nfrom vllm import LLM, SamplingParams llm = LLM(model=\u0026#34;Qwen/Qwen-7B-Chat\u0026#34;) **共享前缀：\u0026#34;你是一个 helpful AI。请回答：\u0026#34;** prompts = [ \u0026#34;你是一个 helpful AI。请回答：巴黎是哪个国家的首都？\u0026#34;, \u0026#34;你是一个 helpful AI。请回答：量子力学是谁提出的？\u0026#34;, \u0026#34;你是一个 helpful AI。请回答：Python 如何读取文件？\u0026#34; ] outputs = llm.generate(prompts, SamplingParams(temperature=0.7)) **→ vLLM 自动共享前缀的 KV Cache！** ⚠️ 注意事项\n问题 说明 前缀必须完全一致 包括空格、标点、换行符。\u0026quot;你好\u0026quot; ≠ \u0026quot;你好 \u0026quot; 不支持动态插入 如果前缀在运行时拼接（如加时间戳），则无法共享 与 LoRA 兼容性 若使用不同 LoRA adapter，即使 prompt 相同也无法共享 仅限 Prefill 阶段 Decode 阶段因输出不同，无法共享 🔧 高级用法：手动控制（vLLM ≥ 0.4.0） 可通过 enable_prefix_caching=True 显式启用（默认已开）：\nllm = LLM( model=\u0026#34;meta-llama/Llama-3-8b\u0026#34;, enable_prefix_caching=True # 默认 True ) 也可通过环境变量：\nVLLM_ENABLE_PREFIX_CACHING=1 🆚 与其他系统的对比\n系统 是否支持前缀共享 vLLM ✅ 是（基于 PagedAttention） HuggingFace TGI ❌ 否（每个请求独立 KV Cache） TensorRT-LLM ✅ 是（需手动构建 shared context） SGLang ✅ 是（RadixAttention 机制） 💡 vLLM 的实现是自动、透明、高效的代表。\n✅ 总结\nvLLM 支持 Prefix Caching，自动优化共享前缀的 batch 请求 无需代码修改，只要 prompt 前缀相同即可生效 显著节省显存 + 提升吞吐，特别适合 RAG、批量指令等场景 确保你的 prompt 模板严格一致（避免尾部空格/换行差异） 如果你正在做 RAG 或批量推理，Prefix Caching 是 vLLM 给你的一份免费性能大礼包！🎉\n问题：但是一个batch内的数据是一起进行并行推理的呀，那么这个时候 cache还没产生吧？ 这是一个非常关键且深刻的问题！你的直觉是对的：如果整个 batch 是“同时”进行 prefill（即并行处理所有 prompt），那似乎还没生成 KV Cache，怎么共享？ 但实际情况是：vLLM 的 Prefix Caching 并不是在“batch 内部并行时”动态发现共享的，而是在调度阶段通过“前缀树（Radix Tree / Trie）”预先组织请求，使得相同前缀的请求在逻辑上被识别并复用已缓存的 KV 块。 下面我们一步步澄清这个看似矛盾的问题：\n✅ 核心答案\nPrefix Caching 的“cache”并不是来自当前 batch 内部的其他请求，而是来自：\n之前已完成的请求（跨 batch 共享），或 在当前 batch 内部，vLLM 调度器会先处理公共前缀一次，再广播给其他请求（逻辑上串行化前缀计算） 但在 纯首次 batch（没有任何历史 cache）且所有请求首次出现 的情况下，同一个 batch 内的请求确实无法彼此共享 KV Cache —— 除非 vLLM 显式地将公共前缀提取出来单独计算一次。 而 vLLM 正是这么做的！ 🔍 深入机制：vLLM 如何在单个 batch 内实现前缀共享？\n关键技术：Radix Tree（基数树） + Block-wise KV Cache Management vLLM 在调度一个 batch 前，会做以下事情：\n步骤 1：构建 Radix Tree\n将 batch 中所有 prompt 插入一棵 前缀树（Trie） 自动识别最长公共前缀（LCP） Prompts: \u0026#34;你是AI。Q: 巴黎?\u0026#34; \u0026#34;你是AI。Q: 量子?\u0026#34; \u0026#34;你是AI。Q: Python?\u0026#34; → Radix Tree: root └── \u0026#34;你是AI。Q: \u0026#34; ├── \u0026#34;巴黎?\u0026#34; ├── \u0026#34;量子?\u0026#34; └── \u0026#34;Python?\u0026#34; 步骤 2：分层 Prefill（Hierarchical Prefill）\n先对公共前缀 \u0026quot;你是AI。Q: \u0026quot; 执行一次 prefill 生成其对应的 KV Cache blocks 再对每个后缀（\u0026ldquo;巴黎?\u0026rdquo; 等）分别执行 prefill 每个后缀的 attention 计算会 拼接公共前缀的 KV blocks + 自身后缀的 KV blocks 🎯 这相当于 把一个长 prompt 拆成 “shared part + unique part”，并在计算时组合。\n步骤 3：PagedAttention 支持非连续 KV 块\n每个请求的完整 KV Cache = [shared_block_ids] + [unique_block_ids] Attention 计算时，PagedAttention 自动从多个物理页读取，对模型透明 📌 重要澄清\n误解 事实 “batch 内所有 prompt 同时并行计算” ❌ 实际是：公共前缀先算一次，后缀再并行（逻辑上分阶段） “cache 必须来自历史请求” ❌ 即使全是新请求，vLLM 也能在 batch 内部构造共享 “Prefill 是完全独立的” ❌ vLLM 的调度器会主动重组计算图以启用共享 🧪 实验验证（思想实验） 假设 batch 有 3 个全新请求，无任何历史 cache：\nprompts = [ \u0026#34;Hello world A\u0026#34;, \u0026#34;Hello world B\u0026#34;, \u0026#34;Hello world C\u0026#34; ] vLLM 的执行流程：\n调度器发现公共前缀 \u0026quot;Hello world \u0026quot; 创建一个虚拟“共享请求”，prefill \u0026quot;Hello world \u0026quot; → 得到 KV blocks S 对每个后缀 \u0026quot;A\u0026quot;, \u0026quot;B\u0026quot;, \u0026quot;C\u0026quot;： 构造输入：\u0026quot;Hello world X\u0026quot; 但 KV Cache = S + prefill(\u0026ldquo;X\u0026rdquo;) 的结果 最终每个请求的输出正确，且 S 只计算了一次 💡 这本质上是一种 计算图融合优化，由 vLLM 的 scheduler 自动完成。\n📚 官方依据\nvLLM 论文（vLLM: Easy, Fast, and Cheap LLM Serving）提到： \u0026ldquo;PagedAttention enables sharing of memory blocks across requests with common prefixes.\u0026rdquo;\n源码中 RadixAttention（后续演进）和 PrefixCacher 组件明确支持 intra-batch prefix sharing 🔗 参考：vLLM Prefix Caching Design Doc\n✅ 总结\n问题 回答 同一个 batch 内的请求能共享前缀吗？ ✅ 能！即使全是新请求 cache 是哪里来的？ 来自 当前 batch 内部对公共前缀的首次计算（调度器主动提取） 是否需要历史请求？ ❌ 不需要（跨 batch 共享是额外 bonus） 是否破坏并行性？ ⚠️ 公共前缀计算是串行的，但后缀仍可并行，整体吞吐仍大幅提升 💡 所以你的观察很敏锐，但 vLLM 通过 智能调度 + Radix Tree + PagedAttention 解决了这个“鸡生蛋”问题。 这正是 vLLM 相比传统推理引擎（如 HF Transformers）的先进之处！\nP04 rag-best-practices 项目原理和结果 参考项目 chaoql/rag-best-practices: 大模型检索增强生成技术最佳实践。\nQdrant的代码组织非常糟糕。 QdrantVectorStore 里面还套了一个fastembed模型。 对于模型下载模块的问题定义了独立的缓存目录，但是又和hf的目录混到了一块。并且函数接口写得很不通用。 所以放弃这块功能的跑通。 原理 所谓混合检索就是快慢结合，fastembed(sparse embed) 和denseembed HyDE Hypothetical Document Embeddings（HyDE） 是一种提升检索质量的高级嵌入技术，由 Gao et al. 在 2022 年提出（论文：Precise Zero-Shot Dense Retrieval without Relevance Labels）。它的核心思想是： 不直接用用户查询（query）去检索，而是先让大语言模型（LLM）生成一个“假设性答案”（hypothetical document），再用这个假设答案的嵌入向量去检索真实文档。 这个项目中的hybrid 模式的意思是，query生成一个稀疏向量、一个稠密向量，两种同时请求数据库，而稀疏向量可能是按照传统倒排索引来进行索引构建的，即最终既有 倒排索引搜索 又有向量搜索。 当然Qrant 本身也允许请求带 filter，即关键词检索、类别检索等。 embedding 模型微调过程 对于输入数据文件中的每一条数据，通过chatglm模型，构造prompt（其中包含一条文档），要求模型返回一些和上下文相关的问题，即形成了Answer-Question 对。 然后反转A-Q对（映射关系为n-to-1），构造出微调数据。 总体来说，就是依赖一个超大的chat大模型和一个文档数据集，构造出Q-A相似对，进而将chat大模型的对该文档的相关知识（对文档数据集领域的知识理解更透彻、更精细） 微调到一个emb小模型内。 模型微调使用的是llama-index 中 embedding 模型微调的功能类 llama_index.finetuning.embeddings.sentence_transformer.SentenceTransformersFinetuneEngine。 其中损失函数是 MultipleNegativesRankingLoss MultipleNegativesRankingLoss 对于本文的数据形式 即 Q-A对，即将A视作一个正样本，同时采样 in-batch内的其他样本作为多个负样本，来计算出一个 Softmax Cross-Entropy Loss，然后再Q维度上取均值即为batch损失。 Configuring the Response Mode 模型下载 HuggingfaceEmbedding 类 如何解决外网不可访问的问题。 使用 modelscope下载模型，然后制定 HuggingfaceEmbedding的模型加载路径为 modelscope缓存中模型的绝对路径。（不是模型缓存的父目录，而是具体到一个模型的目录） modelscope上的模型不全，有些知名度不高的模型 hf上游，但modelscope上缺少。 服务器 阿里云服务器可以使用 modelscope 来进行下载，然后然后模型加载都使用绝对路径来加载。 本地开发 可以使用 huggingface 来进行下载和管理。 FlagEmbeddingReranker “Flag” in FlagEmbeddingReranker = BAAI（智源研究院）的开源模型品牌标识，意为“旗舰级嵌入/重排模型”，无特殊技术含义，主要是项目命名的一部分。 modelscope的模型目录架构和huggingface的模型目录结构 差异很大，不能直接复用。所以如果要加载本地模型，务必使用模型绝对路径。 工程 Qdrant 高级过滤能力： 支持结合向量相似性搜索和任意 Payload 结构化数据过滤（称为 Hybrid Search）。 node_parser llama_index.core.node_parser.text.sentence_window.SentenceWindowNodeParser 按照句子进行切割，多个句子作为一个索引文档。 Node Parser Modules | LlamaIndex Python Documentation pickle方式不安全是因为 __reduce__方法中可以定义一个 指令字节流，在反序列化的时候可以触发对应指令的执行。 问题：pytorch_model.bin 和 model.safetensors 分别是什么 pytorch_model.bin 和 model.safetensors 是两种常见的用于存储深度学习模型权重（参数）的文件格式，广泛用于 Hugging Face Transformers 等生态中。它们的核心目的相同：保存神经网络的可训练参数，但在设计哲学、安全性、性能和兼容性方面有显著区别。\n1. pytorch_model.bin\n是什么？\n这是 PyTorch 原生的模型权重保存格式。 本质上是一个通过 torch.save() 保存的 Python pickle 文件，通常包含一个字典，键为参数名（如 \u0026quot;transformer.h.0.attn.c_attn.weight\u0026quot;），值为对应的 torch.Tensor。 特点：\n方面 说明 格式 基于 Python 的 pickle 序列化 兼容性 只能在 PyTorch 环境中加载（需匹配版本） 安全性 ❗ 不安全：pickle 可执行任意代码，加载不受信任的 .bin 文件可能导致远程代码执行（RCE）漏洞 速度 加载/保存较快，但依赖 Python 反序列化 跨语言 不支持（强依赖 Python 和 PyTorch） 典型用途 Hugging Face 模型仓库中的默认权重格式（尤其旧模型） 示例加载方式：\nimport torch state_dict = torch.load(\u0026#34;pytorch_model.bin\u0026#34;, map_location=\u0026#34;cpu\u0026#34;) model.load_state_dict(state_dict) ⚠️ 警告：永远不要加载来自不可信来源的 pytorch_model.bin！\n2. model.safetensors\n是什么？\n由 Hugging Face 和 CarperAI 联合开发的一种安全、快速、框架无关的模型权重存储格式。 不使用 pickle，而是将张量数据以原始二进制形式存储，并附带 JSON 元数据描述张量结构。 特点：\n方面 说明 格式 自定义二进制格式 + JSON header（无代码执行） 安全性 ✅ 安全：仅包含数值数据，无法执行任意代码 速度 加载通常比 .bin 更快（尤其大模型），支持内存映射（mmap） 跨框架 支持 PyTorch、TensorFlow、Flax、JAX 等（只要能读取 NumPy-like 张量） 文件大小 与 .bin 几乎相同（未压缩，但可选 gzip） 典型用途 Hugging Face 新模型推荐格式，尤其在公开分享或 Web 服务中 文件结构简述：\n[JSON metadata header (UTF-8, padded to 8-byte align)] { \u0026#34;weight1\u0026#34;: {\u0026#34;dtype\u0026#34;: \u0026#34;F32\u0026#34;, \u0026#34;shape\u0026#34;: [1024, 1024], \u0026#34;data_offsets\u0026#34;: [0, 4194304]}, \u0026#34;weight2\u0026#34;: {\u0026#34;dtype\u0026#34;: \u0026#34;F16\u0026#34;, \u0026#34;shape\u0026#34;: [512], \u0026#34;data_offsets\u0026#34;: [4194304, 4195328]} } [Raw binary tensor data (concatenated)] 示例加载方式（PyTorch）：\nfrom safetensors.torch import load_file state_dict = load_file(\u0026#34;model.safetensors\u0026#34;, device=\u0026#34;cpu\u0026#34;) model.load_state_dict(state_dict) 对比总结\n特性 pytorch_model.bin model.safetensors 底层机制 Python pickle 自定义二进制 + JSON 安全性 ❌ 危险（可执行代码） ✅ 安全（纯数据） 加载速度 快 通常更快（支持 mmap） 跨平台/语言 否（PyTorch + Python） 是（多框架支持） Hugging Face 推荐 旧模型默认 新模型首选 文件扩展名 .bin .safetensors 实际建议\n✅ 优先使用 .safetensors：尤其当你从网上下载模型、部署到生产环境或提供 API 服务时。 🔒 避免加载未知来源的 .bin 文件：除非你完全信任提供者。 🔄 很多模型仓库同时提供两种格式（如 Llama 3、Mistral），你可以自由选择。 🛠️ 可用工具转换： # 使用 Hugging Face transformers 自动转换 from transformers import AutoModel model = AutoModel.from_pretrained(\u0026#34;your-model\u0026#34;, use_safetensors=True) # 自动优先加载 .safetensors 如果你正在开发一个需要加载外部模型的应用（如 Web UI、推理服务），强烈建议只支持 .safetensors 格式以规避安全风险。\n问题：Sparse text embeddings 和 dense text embeddings 的区别 Sparse text embeddings 和 dense text embeddings 是两种不同的文本表示方法，它们在结构、生成方式、应用场景和性能特点上有显著区别。以下是详细对比：\n一、核心定义\n类型 定义 Dense Embeddings（稠密嵌入） 将文本映射为一个低维（通常几百到几千维）、连续、稠密的实数向量，几乎所有维度都有非零值。 Sparse Embeddings（稀疏嵌入） 将文本表示为一个高维（可能数万到百万维）、离散、稀疏的向量，绝大多数维度为 0，只有少数维度有非零值（通常是正权重）。 二、典型代表\n类型 模型/方法示例 Dense - BERT, RoBERTa - Sentence-BERT (SBERT) - BGE (BAAI General Embedding) - OpenAI Embeddings - Cohere Embeddings Sparse - BM25（传统，非学习型） - SPLADE - uniCOIL - DeepImpact - ColBERT（部分稀疏，带后期交互） 💡 注意：BM25 虽然常被归为“稀疏检索”，但它不是“embedding 模型”（无神经网络），而 SPLADE 等是基于深度学习的稀疏 embedding 模型。\n三、结构对比（举例） 假设词汇表大小为 30,000：\nSparse embedding（如 SPLADE）： [0, 0, 0, ..., 2.1, 0, 0, ..., 0.8, ..., 0] # 长度=30,000，仅几十个非零值 → 每个非零位置对应一个词（如 \u0026ldquo;apple\u0026rdquo; 在索引 12345），值表示重要性。 Dense embedding（如 BGE）： [0.23, -0.41, 0.88, ..., 0.05] # 长度=1024，几乎每个值都非零 → 向量维度与词汇表无关，语义信息分布在整个向量中。 四、关键区别总结\n维度 Sparse Embeddings Dense Embeddings 维度 高维（≈词汇表大小，如 30k–1M） 低维（通常 384–4096） 非零元素 极少（\u0026lt; 100 个） 几乎全部非零 可解释性 ✅ 高（每个非零维对应一个词） ❌ 低（语义分布在隐空间） 存储效率 ✅ 高（可用倒排索引压缩存储） ⚠️ 中等（需存储完整浮点数组） 检索方式 倒排索引（Inverted Index） + TF-IDF/BM25 风格打分 向量相似度（如余弦、内积）+ ANN（如 FAISS, HNSW） 训练目标 优化词级重要性（如通过 KL 散度对齐 BM25） 优化句子/段落级语义相似度（如对比学习） 多语言支持 依赖词汇表，跨语言较难 更容易通过共享语义空间实现 硬件加速 CPU 友好，适合大规模文档库 GPU/ANN 加速效果更好 五、应用场景\n✅ Sparse 更适合：\n大规模文档检索（如搜索引擎）：==利用成熟的倒排索引技术==，高效过滤海量文档。 关键词敏感任务：需要精确匹配术语（如法律、医疗检索）。 资源受限环境：无需 GPU，纯 CPU 即可高效运行。 ✅ Dense 更适合：\n语义相似度任务：理解同义、 paraphrase、上下文含义（如 “car” ≈ “automobile”）。 小样本或 zero-shot 场景：泛化能力强。 RAG 系统中的精排：与 LLM 结合效果好。 六、混合方法（Hybrid Retrieval） 现代系统常结合两者优势：\nQuery → [Sparse Retriever (BM25/SPLADE)] → Top K candidates + [Dense Retriever (BGE)] → Top K candidates → 合并去重 → Reranker (e.g., BGE-reranker) ColBERT：虽输出稠密 token 向量，但通过 MaxSim 实现类似稀疏的细粒度匹配。 BGE-M3（BAAI 最新模型）：同时输出 dense、sparse 和 multi-vector 表示，支持统一检索。 七、可视化比喻\nSparse：像一本索引目录——只列出关键词及其页码（位置+权重）。 Dense：像一幅抽象画——整体表达语义，无法拆解单个元素含义。 总结一句话：\nSparse embeddings = 关键词加权（可解释、高效）；Dense embeddings = 语义压缩（泛化强、需向量检索）。 根据你的任务需求（精度 vs 效率、关键词匹配 vs 语义理解），可以选择合适类型，或直接使用 hybrid 方案获得最佳效果。\n问题：Sparse Embeddings的模型如何做到 输出稀疏向量的 这是一个非常好的问题！Sparse Embeddings 模型（如 SPLADE、uniCOIL、DeepImpact 等）之所以能输出“稀疏但有信息量”的向量，关键在于它们通过神经网络学习每个词项（term）的重要性权重，并强制绝大多数权重为零或接近零。下面从原理、技术手段和训练目标三个方面详细解释：\n一、核心思想：将文本映射到“词汇表维度”的稀疏向量 与 dense embedding 输出一个固定低维向量不同，sparse embedding 模型的输出维度 = 词汇表大小（V）（例如 V = 30,000）。\n对于输入文本，模型会为词汇表中的每个词 $w_i$ 预测一个 非负重要性分数 $s_i \\geq 0$，最终输出向量为： $$ \\mathbf{e} = [s_1, s_2, \u0026hellip;, s_V] $$ 其中 绝大多数 $s_i = 0$，只有少数关键词的 $s_i \u0026gt; 0$，形成稀疏表示。\n✅ 这个向量可以直接用于 倒排索引（Inverted Index）检索，就像 BM25 一样高效。\n二、如何实现“稀疏性”？关键技术手段\n1. 使用带稀疏诱导的激活函数 最常用的是 ReLU + 对数变换 + FLOPS 正则化（以 SPLADE 为代表）：\n（1）基于 BERT 的 token-level logits\n输入文本 → BERT 编码器 → 得到每个 token 的上下文表示。 通过一个线性层（或 MLM head）预测 整个词汇表上每个词的 logits（类似掩码语言建模）。 （2）应用 $\\text{ReLU}(\\log(1 + \\exp(x)))$ 或直接 $\\text{F}(x) = \\max(0, x)$\n将 logits 经过 非负激活，确保输出 ≥ 0。 实际中常用：\n$$s_i = \\text{ReLU}\\left( \\text{MLM logits}_i \\right)$$ 或更平滑的： $$ s_i = \\log\\left(1 + \\exp(\\text{MLM logits}_i)\\right) \\quad \\text{(softplus)} $$ 📌 注意：即使 logits 是稠密的，后续会通过正则化让大多数 $s_i \\to 0$。\n2. 引入稀疏性正则化（Sparsity Regularization） 这是实现“真正稀疏”的关键！模型在训练时会惩罚非零元素的数量或大小。\n常见正则项（加在 loss 中）：\nL1 正则化：$\\lambda \\sum_i |s_i|$\n→ 鼓励权重趋近于 0。 FLOPS 正则化（SPLADE 提出）：\n$$ \\mathcal{L}_{\\text{flops}} = \\left( \\frac{1}{V} \\sum_i \\mathbb{E}[s_i] \\right)^2 $$ → 惩罚平均激活强度，促使模型只激活极少数词。 Group Lasso / Top-k 约束：强制只保留 top-k 个最大权重。 💡 训练完成后，推理时可设置阈值（如 $s_i \u0026lt; 1e-4$ 则置 0），进一步提升稀疏度。\n3. 利用预训练语言模型的 MLM Head\nSPLADE 等模型直接复用 BERT 的 Masked Language Model (MLM) 头。 MLM 头本来就能预测“哪些词可能出现在上下文中”，天然具有词级语义感知能力。 通过微调 MLM 头，使其输出的 logits 不再用于预测被遮盖词，而是作为全文的词重要性分布。 ✅ 这样既利用了预训练知识，又实现了端到端的稀疏表示学习。\n三、训练目标：对齐相关性信号 为了让稀疏向量“有信息量”（即能有效排序相关文档），模型通常采用以下监督信号：\n方法 训练目标 SPLADE 最大化查询-正样本文档的相似度（内积），最小化查询-负样本的相似度 + 稀疏正则 DeepImpact / uniCOIL 回归到伪标签（如 BM25 分数、教师模型打分） 无监督 SPLADE 使用对比学习或自监督（如文档-标题对） 相似度计算方式： $$ \\text{score}(q, d) = \\sum_{i=1}^V \\text{ReLU}(q_i) \\cdot \\text{ReLU}(d_i) $$ → 本质是 加权词重叠（Weighted Term Matching），但权重由神经网络学习得到，比 TF-IDF/BM25 更智能。 四、举个具体例子：SPLADE 如何工作？\n输入查询 \u0026quot;How to bake a cake?\u0026quot; 经过 BERT + MLM head → 输出 30,000 维 logits 应用 $\\text{ReLU}(\\cdot)$ → 得到非负分数 加入 FLOPS 正则 → 模型学会只给 \u0026quot;bake\u0026quot;, \u0026quot;cake\u0026quot;, \u0026quot;recipe\u0026quot; 等词高分，其余为 0 最终输出稀疏向量：仅几十个非零值，每个对应一个关键词及其语义重要性 🔍 检索时： 文档也用同样方式编码成稀疏向量 通过倒排索引快速计算内积得分（只计算非零维度交集） 五、为什么叫“响亮”（informative）？\n虽然稀疏，但非零维度是模型精心挑选的关键词，且权重反映其在当前上下文中的语义重要性。 相比 BM25 的静态 TF-IDF，sparse embedding 能： 理解同义词（\u0026ldquo;car\u0026rdquo; → 激活 \u0026ldquo;automobile\u0026rdquo;） 抑制停用词（\u0026ldquo;the\u0026rdquo;, \u0026ldquo;how\u0026rdquo; 权重≈0） 根据上下文动态调整（\u0026ldquo;apple\u0026rdquo; 在科技 vs 水果语境下激活不同词） 总结\n关键点 说明 输出维度 = 词汇表大小（高维） 稀疏性来源 ReLU/Softplus 激活 + L1/FLOPS 正则化 信息量来源 基于预训练 MLM 头 + 相关性监督学习 检索兼容性 完全兼容传统倒排索引系统（如 Lucene, Elasticsearch） 代表模型 SPLADE（最经典）、uniCOIL、DeepImpact、BGE-M3（支持 sparse 输出） ✅ 所以，“稀疏但响亮” = 只保留最关键的几个词，但每个词的权重都经过深度语义理解优化。 如果你正在构建检索系统，sparse embeddings 能让你在不牺牲太多语义能力的前提下，享受传统搜索引擎的高效与可扩展性。\n其他问题 问题：langchain中的竖线运算符是由哪些方法来实现的 在 langchain 中，竖线运算符（|）的实现主要依赖于以下方法：\n核心实现方法\n__or__ 方法 这是 Python 中实现竖线运算符的核心魔术方法 当使用 a | b 语法时，Python 会调用 a.__or__(b) __ror__ 方法 右侧对象的反向或运算方法 当左侧对象没有实现 __or__ 时调用 在 Langchain 中的应用 在你的代码中 prompt|llm|output_parser 这个链条中：\nprompt（ChatPromptTemplate 对象）实现了 __or__ 方法 llm（ChatOpenAI 对象）也实现了 __or__ 方法 output_parser（StrOutputParser 对象）同样支持链式操作 实现机制\n**伪代码示例** class BaseComponent: def __or__(self, other): return Chain(self, other) 这种设计模式允许不同的 langchain 组件（如 prompt、llm、output_parser）通过竖线运算符组合成一个完整的处理链 chain。\n问题：langchain中的StrOutputParser 是如何自动对 chatbot的结果进行渲染的 作为一名资深技术导师，我来详细解释 LangChain 中的 StrOutputParser 不是如何“自动渲染”结果的，而是它在 LangChain 表达式语言 (LCEL) 管道中扮演的关键角色。\n核心观点：StrOutputParser 并非渲染器，而是转换器 StrOutputParser 的作用不是对结果进行复杂的“渲染”（如格式化为 Markdown 或 HTML），而是执行一个非常简单但至关重要的类型转换： 它确保管道（Chain）的最终输出是标准的 Python 字符串（str）类型。 在 LangChain 管道中，“渲染”这个词通常意味着将输出从一个复杂对象转换为用户或下一个组件可以使用的格式。\n1. 为什么需要 StrOutputParser？ LLM 模型的原始输出通常是一个复杂的对象，而不是一个简单的字符串：\n原始输出类型 解释 BaseMessage 对于 Chat 模型 (ChatOpenAI, ChatGroq 等)，它们的 invoke() 或 stream() 方法返回的是一个 BaseMessage 对象（通常是 AIMessage 或 HumanMessage 的子类）。 Generation 对于 Completion 模型 (OpenAI 等)，它们的 invoke() 返回的是一个 Generation 对象的列表（包装在 LLMResult 中）。 StrOutputParser 的工作就是从这些复杂的对象中，安全地提取出人类可读的字符串内容。 2. StrOutputParser 的工作机制 StrOutputParser 在 LCEL 管道中实现了 BaseOutputParser 接口，它的核心逻辑非常简单：\nA. 处理 Chat 模型输出 (BaseMessage 对象) 当它接收到 AIMessage 或其他 BaseMessage 对象时，它会调用该对象的 .content 属性。\n输入示例： \u0026lt;AIMessage content='你好！我是一个AI。'\u0026gt; 输出结果： '你好！我是一个AI。' B. 处理 Completion 模型输出 (LLMResult / Generation 对象) 如果它接收到的是 Completion 模型返回的结构，它会提取第一个 Generation 对象的文本内容。\nC. 处理流式输出 (Streaming) 在流式传输中，StrOutputParser 会持续接收到片段（Chunks）的 BaseMessage 对象或字符串。它的作用是：\n提取内容： 从每个传入的 Chunk 中提取 .content 属性。 拼接内容： 将这些内容片段安全地拼接起来，形成最终的完整字符串。这确保了前端或用户接收到的流是连续且正确的文本。 3. 如何“自动渲染”的误解 用户之所以觉得它是“自动渲染”的，是因为它将一个不透明的 LangChain 内部对象（如 AIMessage）转换成了应用程序前端可以直接显示的 Python str 类型。 在 LCEL 管道中，StrOutputParser 通常位于管道的末端，因为它确保了管道的输出能够被后续的系统（如 Flask API 响应、Streamlit 界面或简单的 print() 语句）轻松消费。\n总结 StrOutputParser 是 LangChain 管道中一个类型转换组件，它的作用是：\n从 LLM 返回的复杂对象（如 AIMessage）中提取纯文本内容。 确保 Chain 的输出类型始终是标准的 str，以便于后续处理和展示。 在流式传输中，负责将接收到的内容片段安全拼接。 问题：那么streamlit的write方法是如何将markdown格式文本自动渲染为有格式的html的 代码 streamlit.elements.write.WriteMixin.write 中有讲解这部分内容。\n这是一个非常好的问题，它触及了 Streamlit 的核心机制和前端交互原理。 streamlit.write() 方法自身不会执行 Markdown 渲染，它是一个“万能”命令，会根据传入的数据类型进行分派（Dispatch）。当它接收到字符串时，它会将渲染工作委托给专门的函数。 下面是 st.write() 如何将 Markdown 文本自动转换为 HTML 格式化显示的详细过程。\n1. st.write() 的分派机制 (The Swiss Army Knife) st.write() 被称为 Streamlit 的“瑞士军刀”，它会检查传入的参数类型：\n传入参数类型 st.write() 实际调用 str (字符串) 委托给 st.markdown() pd.DataFrame 委托给 st.dataframe() dict 或 list 委托给 st.json() Altair/Plotly Chart 委托给相应的图表函数 Exception 委托给 st.exception() 因此，当你传入一个包含 Markdown 格式的字符串时，实际执行渲染的是 st.markdown() 函数。 2. st.markdown() 的核心渲染过程 st.markdown() 是专门负责将 Markdown 文本转换为 HTML 的函数。这个转换过程在 Streamlit 内部是这样运作的：\nA. 后端（Python）处理：使用 Markdown 解析库\n解析器使用： Streamlit 的 Python 后端不会自己编写 Markdown 转换逻辑。它会依赖一个或多个成熟的、高性能的 Python Markdown 解析库（例如，早期版本可能依赖 markdown-it-py 或类似的库）。 生成 HTML/抽象结构： 当你调用 st.markdown(body) 时，body 字符串会被这个内部的 Markdown 解析器处理，并转换成对应的 HTML 结构（或一个包含渲染指令的中间数据结构）。 例如，**bold** 被转换为 \u0026lt;strong\u0026gt;bold\u0026lt;/strong\u0026gt;。 序列化与传输： Streamlit 将这个 HTML 结构或渲染指令序列化（通常为 JSON 格式）。 WebSocket 传输： 序列化后的数据通过 WebSocket 连接，从 Python 后端发送到 Streamlit 的前端（浏览器）。 B. 前端（浏览器）渲染：React + CSS\n前端接收： Streamlit 的前端（使用 React 框架构建）接收到通过 WebSocket 发送过来的指令/数据。 DOM 操作： React 组件根据收到的 HTML 结构或指令，将其安全地插入到 Web 页面的 DOM (Document Object Model) 中。 浏览器渲染： 浏览器识别这些 HTML 标签（如 \u0026lt;h1\u0026gt;, \u0026lt;p\u0026gt;, \u0026lt;strong\u0026gt;）并应用默认或 Streamlit 自定义的 CSS 样式。 样式应用： Streamlit 具有内置的样式表，确保所有的 Markdown 元素（标题、列表、代码块等）都遵循应用的主题和外观，看起来美观且一致。 3. 为什么是自动的？ 对于开发者来说，整个过程是自动且无感的，因为：\n封装： st.write() 和 st.markdown() 封装了从 Markdown 字符串到 HTML 渲染的所有步骤。 实时同步： Streamlit 的架构本身就是为了在 Python 后端代码与浏览器前端之间提供实时同步。当你调用一个 Streamlit 命令时，Python 框架会立即处理数据并告诉前端如何更新。 问题：Pydantic 中 BaseModel 的核心作用：数据模型和数据校验 在 Python 的 pydantic 库中，BaseModel 是构建数据模型的基石。它的主要作用是定义数据的结构、类型，并自动进行验证。 简单来说，BaseModel 将松散的 Python 字典数据提升为具有明确结构、类型和内置验证的强大数据对象。\n🚀 BaseModel 的主要功能\n1. 定义清晰的数据结构 (Schema Definition) BaseModel 允许你使用标准的 Python 类型注解来定义你的数据对象应该长什么样，包括哪些字段、每个字段的类型。\n示例: Python from pydantic import BaseModel from datetime import datetime class User(BaseModel): id: int name: str = \u0026#34;John Doe\u0026#34; # 带有默认值 signup_ts: datetime | None = None # 联合类型 is_active: bool 这个类清楚地定义了一个用户对象必须包含 id（整数）、name（字符串）、signup_ts（日期时间或 None）和 is_active（布尔值）。 2. 自动数据校验 (Data Validation) 这是 BaseModel 最核心的功能。当你尝试创建一个 BaseModel 实例时，Pydantic 会自动检查传入的数据是否符合你定义的类型和结构。\n如果数据类型正确： 它会将输入数据（比如 JSON 字符串、字典）转换为正确的 Python 类型（例如将 JSON 字符串 \u0026ldquo;1\u0026rdquo; 转换为整数 1）。 如果数据校验失败： 它会抛出一个清晰的 ValidationError，告诉你哪个字段出了问题，以及为什么。这极大地减少了运行时错误和调试时间。 3. 自动数据转换 (Data Coercion) BaseModel 不仅进行校验，还尝试进行合理的类型转换：\n字符串到数字： 字符串 \u0026quot;123\u0026quot; 会被自动转换为整数 123。 字符串到日期/时间： 遵循 ISO 格式的日期字符串会被自动转换为 datetime 对象。 4. 强大的序列化和反序列化 BaseModel 实例内置了将数据对象转换为其他格式的方法：\n序列化 (Serialization): user_instance.model_dump(): 将模型实例转换为标准的 Python 字典 (dict)。 user_instance.model_dump_json(): 将模型实例转换为 JSON 字符串，方便网络传输。 反序列化 (Deserialization): User(**data_dict): 从字典创建模型实例。 User.model_validate(data_dict): 从字典或 JSON 数据中验证并创建模型实例。 5. 与 FastAPI 等框架集成 在 FastAPI 中，BaseModel 是定义请求体 (Request Body) 和 响应体 (Response Body) 的标准方式。\n当用作请求体时，FastAPI 自动使用 BaseModel 来校验客户端发送的 JSON 数据。 当用作响应体时，FastAPI 自动使用 BaseModel 来序列化你的 Python 对象为 JSON 响应。 总结: BaseModel 的作用就是将 Python 应用程序中的数据处理从松散、易错的字典操作，升级为结构化、类型安全且自带验证的强大对象模型。 问题：llama2并非是专属的embedding模型，也可以用OllamaEmbeddings来加载么？ 这是一个非常专业和关键的问题，它涉及到大模型的多功能性和 Ollama 平台的灵活性。 答案是：是的，您可以将 Llama 2 (或 Llama 3 等聊天/生成模型) 用作 OllamaEmbeddings 的模型，但它并非最佳实践。 以下是详细的解释和建议：\n1. 为什么 Llama 2 可以用于嵌入？（技术可行性） Ollama 平台提供了一个通用的 /api/embeddings REST API 端点。 当您在 OllamaEmbeddings 中指定一个非专用的聊天模型（如 llama2）时，Ollama 守护进程会执行以下操作：\n提取隐藏状态 (Hidden State): Ollama 会运行 Llama 2 模型，但不执行最终的**语言模型头（LM Head）**层（该层负责预测下一个词元）。 池化 (Pooling): 它会从模型的倒数第二层（或根据配置的特定层）提取词元隐藏状态（Token Hidden States）。然后，它通常会应用一个池化策略（如平均池化）将这些词元向量聚合为一个单一的句子向量，作为最终的嵌入（Embedding）。 LangChain 的 OllamaEmbeddings 通过调用这个通用的 Ollama API，实现了对 Llama 2 的嵌入功能调用。 2. 为什么 Llama 2 不推荐用于嵌入？（效果考量） 尽管技术上可行，但 Llama 2 或其他聊天模型（如 Mistral）在没有经过特定微调的情况下，作为嵌入模型的效果通常远不如专用嵌入模型。\n嵌入模型类型 Llama 2 (生成模型) 专用嵌入模型 (如 Nomic Embed, mxbai-embed-large) 训练目标 生成文本（预测下一个词元）。 语义相似性（训练目标是让语义相近的句子在向量空间中距离更近）。 效果/精度 较差。 向量空间没有针对相似性搜索进行优化，检索精度低。 优秀。 在 MTEB 等基准测试中表现优异，RAG 效果好。 资源消耗 高。 Llama 2 尺寸较大（如 7B），推理慢，内存占用高。 低。 专用模型通常更小（如 300M），速度快，资源消耗低。 简而言之： Llama 2 生成的向量可能无法准确捕捉不同句子之间的语义相似性，这会严重损害您的 RAG 检索质量。 3. 架构师建议 作为一名资深架构师，我的建议是：\n对于 RAG 或语义搜索： 始终优先使用专用的嵌入模型。 您已经看到了，Ollama 支持许多高性能、小巧且专门为嵌入任务优化的模型（如 mxbai-embed-large 或 embeddinggemma）。这能提供更好的精度和更快的速度。 实验和自定义： 如果您只是进行实验，或者您的 Llama 2 是经过专门的 对比学习（Contrastive Learning）微调版本（例如，AnglE-LLaMA 等），那么使用 Llama 2 是有意义的。在这种情况下，你需要确保你的 Llama 2 模型文件在 Ollama 中已经包含了必要的嵌入配置。 结论：使用 OllamaEmbeddings(model=\u0026quot;llama2\u0026quot;) 是可行的，但为了您的应用性能和准确性，请使用一个专用的 Ollama 嵌入模型。 ","permalink":"https://chesterwang.github.io/chester-blog/posts/2025-11-07-rag%E9%A1%B9%E7%9B%AE%E5%BC%80%E5%8F%91%E8%AE%B0%E5%BD%95/","summary":"项目环境搭建 如何将requirements.txt中的依赖转移到 uv环境中 uv add -r requirements.txt 如果太慢则加 url uv add -r requirements.txt --index-url https://pypi.tuna.tsinghua.edu.cn/simple ollama搭建 报错 ollama修改模型下载目录之后报错 error: pull model manifest: 503: no healthy upstream 重启服务即可 sudo systemctl stop ollama sudo systemctl start ollama 修改模型目录 sudo ln -s /home/chester/ollama/models /usr/share/ollama/.ollama/ sudo chown ollama /usr/share/ollama/.ollama/models sudo chgrp ollama /usr/share/ollama/.ollama/models 后两条是保证目录的权限问题，不然o","title":"个人RAG项目开发记录"},{"content":"Hugging Face Hub Models, Spaces, and Datasets are hosted on the Hugging Face Hub as Git repositories Do you have files larger than 10MB? Those files should be tracked with git-lfs, which you can initialize with: git lfs install Note that if your files are larger than 5GB you’ll also need to run: hf lfs-enable-largefiles . Pull Request 之所以叫这个名字，是因为它准确地描述了请求者（你）和被请求者（项目维护者）之间的动作和方向。 PR = “我改好了，请你把我这边的修改拉（pull）进你的主分支吧。”这个 “pull” 并不是指你自己去拉，而是 请求项目维护者去拉你的代码。 Templates transformers quickstart load a pretrained model run inference with Pipeline fine-tune a model with Trainer Auto Classes Instantiating one of AutoConfig, AutoModel, and AutoTokenizer will directly create a class of the relevant architecture. transformers库中 AutoImageProcessor 实例话出来的processor 的作用都有哪些 一旦你通过 AutoImageProcessor.from_pretrained() 实例化了一个处理器，这个 processor 实例 就成为了一个强大的工具，专门用于准备图像数据，使其能够被特定的预训练视觉模型使用。它的主要作用可以概括为以下几点： 图像标准化 (Normalization) 图像尺寸调整 (Resizing) 和裁剪 (Cropping) 通道格式转换 (Channel Format Conversion) 图像到张量转换 (Image to Tensor Conversion) 批处理 (Batching) 数据增强 (Data Augmentation) transoformer库中 TFAutoModel和AutoModel的区别是什么 AutoModel：用于加载 PyTorch 框架下的模型。 TFAutoModel：用于加载 TensorFlow 2.0 框架下的模型。 FlaxAutoModel: 用于加载 Flax（基于 JAX 的框架）下的模型。 AutoModel 类的后缀 LM CausalLM MaskedLM MaskedGeneration sequenceClassification TokenClassification NextSentencePrediction MultipleChoice Seq2SeqLM QuestionAnswering Backbone A backbone is a model used for feature extraction for higher level computer vision tasks such as object detection and image classification. Data Collator Data collators are objects that will form a batch by using a list of dataset elements as input. These elements are of the same type as the elements of train_dataset or eval_dataset. 我理解 Data Collator 就是把多个列放在一起，就是collator的字面意思，但其实内部还是会有一些具体的数据处理逻辑，比如padding、数据扩增等 pipeline from transformers import pipeline generator = pipeline(model=\u0026quot;openai-community/gpt2\u0026quot;) generator(\u0026quot;I can't believe you did such a \u0026quot;, do_sample=False) [{'generated_text': \u0026quot;I can't believe you did such a icky thing to me. I'm so sorry. I'm so sorry. I'm so sorry. I'm so sorry. I'm so sorry. I'm so sorry. I'm so sorry. I\u0026quot;}] chat with models Templates Diffusers LoRA Add a LoRA to a pipeline with the load_lora_weights() method. Some LoRA’s require a special word to trigger it, such as Realism, in the example below. Check a LoRA’s model card to see if it requires a trigger word. LoRA文件就是一种插件 Sentence Transformers sentence embedding demo\nfrom sentence_transformers import SentenceTransformer # 1. Load a pretrained Sentence Transformer model model = SentenceTransformer(\u0026#34;all-MiniLM-L6-v2\u0026#34;) # The sentences to encode sentences = [ \u0026#34;The weather is lovely today.\u0026#34;, \u0026#34;It\u0026#39;s so sunny outside!\u0026#34;, \u0026#34;He drove to the stadium.\u0026#34;, ] # 2. Calculate embeddings by calling model.encode() embeddings = model.encode(sentences) print(embeddings.shape) # [3, 384] # 3. Calculate the embedding similarities similarities = model.similarity(embeddings, embeddings) print(similarities) # tensor([[1.0000, 0.6660, 0.1046], # [0.6660, 1.0000, 0.1411], # [0.1046, 0.1411, 1.0000]]) trl Quickstart 简单的helloworld程序。 evaluation: The important thing is that this process should yield a scalar value for each query/response pair. 这里的例子代码应该已经过时了，PPOTrainer现在没有step这个方法。 Dataset formats and types How-to guides customozing the training Memory efficient fine-tuning by sharing layers PPO PPO策略中的一些基础知识 深度学习硬件配置中的概念 device rank world_size node 分别是指什么 这四个概念的关系可以概括为：node 是物理机器。 一个 node 可以包含多个 device，device通常是指GPU。 每个 device 通常由一个独立的 rank 进程来控制。 所有 rank 进程的总数就是 world_size。 PPO示例 trl/examples/scripts/ppo/ppo.py at v0.21.0 · huggingface/trl model=policy, ref_model=ref_policy, reward_model=reward_model, value_model=value_model,、 reward_model其实是每一小步决策的即时的、直接奖励，value_model对每一小步决策的全局性、长期性后果进行预测。 advantage 我直观理解是因为Q(s,a) 这个state的随机选择导致后续的所有reward都比较异常，所以要消除一部分state的随机性，所以减去state的value；如果训练数据中的Q(s,a)中的s足够多，那么就可以直接使用Q，但因为不够多有了随机性，所以Q代表的return分布就偏离了所谓的全局性的Q的分布，所以通过减去这个s的随机性从而拉回正常分布。 Mixin 类本身不能独立实例化，它不是用来创建对象的。相反，它像一个“功能包”或“能力插件”，专门设计用来被其他类继承。当一个类继承了一个或多个 Mixin 类时，它就自动获得了这些 Mixin 类所定义的所有方法和属性。 PPO中的数据处理 transformers.tokenization_utils_base.PreTrainedTokenizerBase.batch_encode_plus 方法中包含了 tokenizer中的padding处理。 PPO官方例子中中的代码， 为什么 padding_side 和后面的padding=false ，这两者不冲突么 代码中两个地方使用了tokenizer， PPOTrainer( args=training_args, processing_class=tokenizer,...) 这里的tokenizer定义中是带有padding=\u0026ldquo;left\u0026quot;的。 这里的tokenizer 在PPOTrainer中是作为 processing_class 的作用。 在PPOTrainer中 会根据 processing_class 生成一个 DataCollatorWithPadding 类用于数据进行处理，而该类的计算逻辑是使用tokenizer对数据仅仅进行padding操作（这里的padding是右对齐），这里不包括encoding操作。因为训练数据已经根据第二条中的 prepare_dataset进行了encoding。 prepare_dataset 中的 tokenize函数。 该函数的输入是一条数据，所以不需要进行padding。 总结： 即encodding和padding是分在两个地方进行处理的，分别是PPOTrainer实例化之前 和 PPOTrainer的train方法内部 分别进行encoding和padding。 至于为什么这样做，从代码中没有看出原因。可能是为了更细粒度的控制训练和评估各自场景下的逻辑。 trl.trainer.utils.selective_log_softmax 其中实现针对FP32 FP64 采用了高效的 logsumexp方法，针对其他的格式采用了低效的方法，因为低精度格式计算容易出现问题。 logsumexp 的标准定义是： $$\\log \\sum_{i} \\exp(x_i)$$，如果直接按这个公式实现，当 $x_i$ 的值很大时，$\\exp(x_i)$ 可能会导致数值上溢（overflow），超出浮点数的表示范围，结果变为无穷大。 工程上的稳定实现方法 1. 为了避免上溢，logsumexp 在大多数深度学习框架（如 PyTorch、TensorFlow）中都有一个经过优化的、数值稳定的实现。核心思想是利用对数函数的性质，将指数运算中的大数相加问题，转换为对数运算中的小范围数相加问题。 2. 具体方法如下： 找到最大值：首先，找到输入向量 $x$ 中的最大值 $x_{max}$。 转换公式：利用 $e^{a+b} = e^a e^b$ 的性质，将原公式进行等价转换： $$\\log \\sum_{i} \\exp(x_i) = \\log \\left( \\exp(x_{max}) \\sum_{i} \\exp(x_i - x_{max}) \\right)$$ 拆分对数：利用 $\\log(ab) = \\log(a) + \\log(b)$ 的性质，进一步拆分： $$= \\log(\\exp(x_{max})) + \\log \\left( \\sum_{i} \\exp(x_i - x_{max}) \\right)$$ $$= x_{max} + \\log \\left( \\sum_{i} \\exp(x_i - x_{max}) \\right)$$ 为什么这个方法更稳定？ 防止上溢：在转换后的公式中，$x_i - x_{max}$ 的值都是负数或者零。这意味着 $\\exp(x_i - x_{max})$ 的值都在 $(0, 1]$ 范围内。这样，即使 $x_i$ 非常大，指数运算的结果也不会上溢。 保持精度：虽然 $x_i - x_{max}$ 的值是负数，但它们之间的相对大小关系保持不变，这保证了计算结果的精确度。 计算效率：这个稳定的实现只需要额外进行一次 max 运算和一次加法运算，对整体计算效率影响很小。 例如，在 PyTorch 中，torch.logsumexp 函数就是以这种方式实现的。当你使用它时，框架会自动处理这些数值稳定性的细节。 这个工程优化方法在深度学习中非常重要，特别是在处理诸如 Softmax 交叉熵损失、信念传播（Belief Propagation）等需要大量指数和对数运算的场景。 PPO策略的流程(from chatgpt) ^PPO-process 一、PPO算法核心思想 PPO 属于**策略梯度（Policy Gradient）**家族，目标是通过不断优化策略参数，使得智能体在环境中获得更高的期望回报。 它的关键在于：在更新策略时限制更新幅度，防止策略改变太大导致训练不稳定。 二、PPO的基本流程（典型版本：PPO-Clip） 采样（Rollout） 使用当前策略 $\\pi_{\\theta_{old}}(a_t|s_t)$，与环境交互，收集一批数据： $(s_t, a_t, r_t, s_{t+1})$ 并计算折扣回报 $R_t$ 。 计算优势函数（Advantage Estimate） 通常用 GAE（Generalized Advantage Estimation）： $\\hat{A}t = \\sum{l=0}^{\\infty} (\\gamma\\lambda)^l \\delta_{t+l}$ 其中 $\\delta_t = r_t + \\gamma V(s_{t+1}) - V(s_t)$。 计算重要性比率（ratio） $r_t(\\theta) = \\frac{\\pi_\\theta(a_t|s_t)}{\\pi_{\\theta_{old}}(a_t|s_t)}$ 表示新旧策略在同一动作上的“概率变化”。 构建PPO的目标函数（Clipped Surrogate Objective） $\\large L^{CLIP}(\\theta) = \\mathbb{E}_t \\big[ \\min( r_t(\\theta) \\hat{A}_t, \\text{clip}(r_t(\\theta), 1 - \\epsilon, 1 + \\epsilon) \\hat{A}_t )\\big]$ 当 $r_t(\\theta)$ 偏离 1 太多（超过 ±ε）时，会被截断（clip），防止过度更新。 ε 通常取 0.1～0.2。 优化目标 + 值函数 + 熵正则 实际优化的目标函数通常是三项之和： $L(\\theta) = L^{CLIP}(\\theta) - c_1 L^{VF}(\\theta) + c_2 S[\\pi_\\theta]$ $L^{VF}$：值函数的MSE损失 $S[\\pi_\\theta]$：策略的熵（鼓励探索） 多次小步更新（K epochs） 用同一批采样数据，在每个 mini-batch 上优化 K 轮。 更新旧策略参数 $\\theta_{old} \\leftarrow \\theta$ PPOTrainer中代码的流程总结 ^PPO-code-process\n外循环：总训练batch数 对 policy model 生成的每一个序列样本，输入到reward model ，序列中最后一个token的 hidden state 被输入到一个线性头，最终输出一个标量值，表示序列二分类的打分score。 对 policy model 生成的每一个序列样本，每一个token输出前的vocabulary 分布和 ref_model的分布 两者计算kl散度（使用了近似方法使得计算加速），即输出一个 kl散度序列，序列中每个元素表示对应位置token的kl散度。 将序列级的score 加到kl序列中每一个位置，结果即为 rewards 序列 依次计算广义优势估计 gae，GAE(t)=rewards(t) + gamma*value(t+1) - value(t) + gamma*lam*GAE(t+1)，这里lam是指lambda 计算 return return(t) = GAE(t)+ value(t) 。（这里把value理解成是 t步的状态state下，未来奖励的总和期望，即 状态本身的价值）。 value即 state value function，GAE即 针对的是action。 内循环：针对上述步骤生成的序列样本和对应的return、GAE、概率logp 序列，将样本拆分成 micro batch，执行下列步骤 value 优化的损失函数为 (return - value(t))^2，其实就是约束 优势advantage GAE 本身尽量。 使用当前的policy model 计算action 的新概率值 $\\large \\log p_{\\theta}$ policy gradient损失为 $\\Large - GAE * exp(\\log p_{\\theta}-\\log p_{\\theta_{old}})$ 我想这里这么写是为了数值稳定性，因为一般是让模型输出 logp，然后计算 概率比，那么就直接相减然后取指数。背后其实就是 logsumexp算子。 两个损失加载一起进行反向传播，即会修改当前的policy_model和value_model 输出和保存各种模型 local_rollout_forward_batch_size 是每个节点，在本地进行多批次的训练，每个批次的大小即为 local_rollout_forward_batch_size。\npolicy_model和ref_model在rollout中调用的方法不同 3. policy_model使用trl.trainer.utils.batch_generation（是对所有计算节点的并行批量化计算）。就是在query之后拼接预测出来的response。 1. batch_generation 使用generation_config参数，代码中规定 max_new_tokens=args.response_length，即硬性指定了response长度。 2. response在生成出来之后，会合并所有的立马进行右padding。 3. 其中调用了GenerationMixin的generate方法，该方法的输出为token ids的序列。 4. ref_model 使用 forward。就是将整个query_response拼接结果全部输入到ref_model，一次性得出错位的预测结果。所以对logits索引的时候会往前错一位，并且最后一个位置是不需要使用的。 1. 我想batch_generation是需要进行复杂padding的，从而可能导致每一个小batch生成的输出长度是不一致的。但我想不到为什么这么调用的理由。 5. 总体上PPO有两个地方牵涉到概率的对比 1. 第一个是 policy_model和ref_model两个模型的KL散度，放置policy_model跑太远过于离谱。 2. 第二个是 policy_model和old_policy_model的样本概率对比，但不需要保留old_policy_old这个模型，因为训练中仅仅是使用了 old_policy_model的样本和其样本概率，所以通过old_policy_model一次采样出来一大堆数据之后（其中保留了样本概率 $\\Large p_{theta_{old}}$）， old_policy_model就可以丢弃，仅仅通过使用采样的样本和样本概率来进行policy_model的训练优化。 那么就相当于是两个模型合二为一。\nclass Qwen2ForSequenceClassification(Qwen2PreTrainedModel): The Qwen2 Model transformer with a sequence classification head on top (linear layer).\n[Qwen2ForSequenceClassification] uses the last token in order to do the classification, as other causal models (e.g. GPT-2) do. 计算的时候会找到 response中的 last_non_pad_token，输出对应的logitis，然后经过score方法 进行linear_unit 计算。\nclass AlbertForSequenceClassification(AlbertPreTrainedModel): Albert Model transformer with a sequence classification/regression head on top (a linear layer on top of the pooled output) e.g. for GLUE tasks.\ndataclass 注解是 Python 3.7 及以上引入的一个装饰器，作用是简化类的编写，让类自动获得一些常用方法（如 __init__, __repr__, __eq__ 等），用于表示数据结构。\n为什么 计算ref_logprob 使用 selective_log_softmax方法，按理说KL散度应该是对vocabulary的所有词进行KL散度计算啊？ 理论上，KL散度的定义是： $$K L \\left(\\right. p \\parallel q \\left.\\right) = \\underset{i}{\\sum} p \\left(\\right. i \\left.\\right) log ⁡ \\frac{p \\left(\\right. i \\left.\\right)}{q \\left(\\right. i \\left.\\right)}$$ 这里 (i) 是整个 vocabulary 的所有 token。 实际工程实现（RLHF/PPO场景）： 6. 我们通常只关心模型实际“走出的路径”，即生成的 token 序列。 7. PPO/Reward Modeling 里，KL项是用来约束新模型（policy）不要偏离旧模型（reference/policy）的行为，只需要对“已采样的 token”上的概率分布做约束。\nrewards[[actual_start, actual_end]] += scores 赋值操作，rewards中存放的是kl散度，这里则是将kl散度对应最后一个token位置向右错一位的位置加上一个最终的reward score。 这里的actual_start是样本的index，actual_end是指每一个样本的最后一个token位置+1。\n在 Hugging Face Transformers 中，类名前的 Auto 表示自动选择模型的意思。 8. AutoModelForSequenceClassification 不是一个具体的模型类，而是一个工厂类。 9. 它可以根据你加载的 checkpoint（如 \u0026quot;bert-base-uncased\u0026quot;、\u0026quot;roberta-base\u0026quot;、自定义路径等），自动实例化对应的具体模型类（如 BertForSequenceClassification、RobertaForSequenceClassification 等）。 10. 这让你不需要关心底层是哪个模型，只要传入模型名或路径，它会自动帮你选择正确的模型实现。\nPPO中 value_model一般使用的是 sequence_classification model，比如 示例脚本 中代码如下，value_model和reward_model使用的是同一个模型类的不同实例。\nvalue_model = AutoModelForSequenceClassification.from_pretrained( training_args.reward_model_path, trust_remote_code=model_args.trust_remote_code, num_labels=1 ) reward_model = AutoModelForSequenceClassification.from_pretrained( training_args.reward_model_path, trust_remote_code=model_args.trust_remote_code, num_labels=1 ) 这里举例 transformers.models.qwen2.modeling_qwen2.Qwen2ForSequenceClassification\nPPOTrainer中调用get_reward的时候，使用的是 sequence_classfication_model的 base_model_prefix指向的底层LLM原始模型 、 score方法（score方法就是在baseLLM模型后添加一个线性层，以映射到logits）。其效果就是对一对问答字符串序列输出一串reward数值。\nget_reward方法中的具体逻辑。 lm_backbone底层使用的是基模型，其中 output_hidden_states 表示要把模型中所有层的hidden_states全部进行输出。所以在调用score的时候会只取最后一层的hidden_states，然后输入到score方法中（即再经过一层线性层得到logits）。效果就是对于问答字符串序列中的每一个token都会得到一个reward_logits数值。\noutput = lm_backbone( input_ids=input_ids, attention_mask=attention_mask, position_ids=position_ids, return_dict=True, output_hidden_states=True, use_cache=False, # otherwise mistral-based RM would error out ) reward_logits = model.score(output.hidden_states[-1]) # 这里的output.hidden_states 表示base_model的所有层的输出 # output.hidden_states[-1] 则表示最后一层的输出 # Qwen2ForSequenceClassification 的forward中计算 loss，就是按照最后一个token的最后一层输出 + 真实label 一起计算出交叉熵 return ( reward_logits, reward_logits[ torch.arange(reward_logits.size(0), device=reward_logits.device), sequence_lengths, ].squeeze(-1), sequence_lengths, ) # 这里表示返回 response序列中最后一个合法token的logits输出 transformers.models.qwen2.modeling_qwen2.Qwen2ForSequenceClassification 是一个例子模型，该模型的forward方法中计算了每一个token输出的分类的logits，然后仅仅获取了每一个序列的最后一个 non_padding_token的logits作为输出。\nreward_model得到的一个序列一个reward值，但其实是最后一步的immediate reward。 value_model得到的每一个动作（即token）一个value值（即长期效果的评估的指标）。\nPPOTrainer中 为什么需要有 missing_eos_penalty？\n在 PPOTrainer 中存在 missing_eos_penalty，主要是为了解决 生成结果没有包含终止符（如 eos_token_id）的情况，防止模型生成不完整或异常的响应。\n在文本生成任务中，模型通常会在响应结尾生成一个“终止符”（例如 eos_token_id），表示响应结束。 如果模型没有生成终止符，响应可能： 超长（一直生成到最大长度） 不完整（缺少语法上的结尾） 影响后续评估和训练（如奖励模型、PPO等） 如果响应没有终止符，可能是模型没学会“何时结束”，这种响应一般是不符合任务预期的，需要惩罚。 missing_eos_penalty 就是对这种情况加一个负分，鼓励模型在合适的时候生成终止符。 Approximating KL Divergence\nPPOTrainer中为什么要whiten_rewards 在 PPOTrainer 中，存在一个参数 whiten_rewards，其作用是对奖励（reward）进行归一化/标准化（whitening）。 奖励的尺度和分布直接影响优势的分布，而优势分布又影响梯度更新的稳定性和训练速度。 如果奖励很大或很小，会导致策略梯度很大/很小，影响收敛速度，甚至导致梯度爆炸/消失。\n那么在采样阶段计算的reward、 value等数据，因为在训练阶段这些数据中的很多会重新生成，所以采样阶段很多计算是不是浪费了?\n采样阶段的 value/logprob只用一次，训练阶段会重新算“新策略”的 value/logprob。 for t in reversed(range(gen_length)): nextvalues = values[:, t + 1] if t \u0026lt; gen_length - 1 else 0.0 delta = rewards[:, t] + args.gamma * nextvalues - values[:, t] lastgaelam = delta + args.gamma * args.lam * lastgaelam advantages_reversed.append(lastgaelam) advantages = torch.stack(advantages_reversed[::-1], axis=1) returns = advantages + values advantages = masked_whiten(advantages, ~padding_mask) advantages = torch.masked_fill(advantages, padding_mask, 0) 这里计算return和advantage。return就是 state value的一个真实采样，用来计算 value_loss，即return和估计的value之间的平方损失。advantage 就是 优势，即当前时刻以及每个之后的时刻所计算的advantage的打折之和，用来给策略梯度加权。\n损失函数\nlogprobs_diff = new_logprobs - mb_logprobs ratio = torch.exp(logprobs_diff) pg_losses = -mb_advantage * ratio pg_losses2 = -mb_advantage * torch.clamp(ratio, 1.0 - args.cliprange, 1.0 + args.cliprange) pg_loss_max = torch.max(pg_losses, pg_losses2) pg_loss = masked_mean(pg_loss_max, ~padding_mask[micro_batch_inds]) loss = pg_loss + args.vf_coef * vf_loss 这块代码中为什么没有使用 logp的导数，原因是 $\\Large \\mathbb{E}{\\beta} \\left[ \\frac{\\pi{\\theta}(a|s)}{\\beta(a|s)} Q^{\\pi}(s,a)\\nabla_{\\theta} \\ln \\pi_{\\theta}(a|s) \\right]$ ，其实就是 $\\Large \\mathbb{E}{\\beta} \\left[ \\frac{\\nabla{\\theta} \\pi_{\\theta}(a|s)}{\\beta(a|s)} Q^{\\pi}(s,a) \\right]$，那么对应的损失函数就是以$\\large \\theta$为优化参数的 $\\Large - \\mathbb{E}{\\beta} \\left[ \\frac{\\pi{\\theta}(a|s)}{\\beta(a|s)} Q^{\\pi}(s,a) \\right]$。\nmb_logprobs 在loss中不会反向传播梯度么 因为 mb_logprobs计算的时候是在 with torch.no_grad(): 中。\nvf_losses1 = torch.square(vpred - mb_return) 这里的mb_return 就是后续多步累积的reward值。\n为什么policy loss 和 value function loss 要加起来进行optimize？\n如果你设计了分离的 policy model 和 value model（即 Actor 和 Critic 完全分离），那么确实可以分开 optimize、分开 backward。 但最主流的实现（比如 Huggingface Transformers 的 PPOTrainer）是合一模型，一个模型里有两个输出 head，参数是共享的，所以必须把 loss 合在一起，统一 backward 和 optimize。 优化器（optimizer）会对所有参数做梯度更新，policy head 和 value head的梯度会分别回传到主干和各自 head。 在train 方法内，ref_policy 是自始至终 一致保持不变的。ref_policy 只是避免 policy_model 跑得太远。而样本重用是使用importantce weight来解决的，即 $\\large \\mathbb{E}{\\beta} \\left[ \\frac{ \\pi{\\theta}(a|s)}{\\beta(a|s)} \\right]$ 。\n这个PPO算法的实现，为什么既有kl散度计算作为 reward，同时又有clip操作\nclip objective：控制单次更新不要太大，防止训练不稳定。 KL 散度奖励：保证整体策略不会逐渐偏离参考模型（human-preference-aligned policy）太远。 KL 散度相当于在 奖励层面惩罚策略远离参考模型，避免模型跑偏；这就是所谓的“KL 奖励塑形”。 clip 解决 短期训练稳定性； KL 奖励解决 长期偏移问题，相当于给模型加了个“牵引绳”。 你看到的实现里，KL 散度不是直接当约束用，而是作为奖励项参与回报计算；而 clip 则在优化目标里约束策略更新。这两者是互补关系。\nDPO TRPO中的PO和 DPO中的PO指的不是一个东西？\nTRPO：它的目标是最大化环境奖励。TRPO (Trust Region Policy Optimization) DPO：它的目标是最大化人类偏好。 DPO (Direct Preference Optimization) DPO的直觉化理解\n整体的目标函数为 $$\\large L(\\theta) = \\max_{\\pi_\\theta} \\mathbb{E}{x \\sim D, y \\sim \\pi\\theta} [r(x, y)] - \\beta \\text{KL}[\\pi_\\theta(y|x), \\pi_{\\theta_{\\text{old}}}(y|x)]$$，函数表达的意义是，在一个奖励结构上，policy的结构必须尽量与奖励结构保持一致，但同时不要偏离老的policy太远，后者可以认为是一定程度的正则化。 形象化理解，对于 y变量的概率分布和奖励分布（即横轴是y 的各种取值，纵轴是对应y值行为的概率值大小、奖励大小），那么用图形理解就是两条曲线（只不过一个是归一化的正值，一个是可能正负值均存在）。对于上述目标函数来说，暂且认为奖励分布$r(x,y)$就是真实的训练数据值（但其实是从 正负例response的 contrastive loss拟合出来的值），那么$L(\\theta)$中的唯一变量就是 策略函数 $\\pi_{\\theta}$ ，这个时候目标函数$L(\\theta)$其实就是一个以 $\\pi_\\theta$为自变量的一个函数（当然自变量本身就是概率分布函数，那么这其实就是一个泛函优化问题）；对上述目标函数进行变换，可以得出 $L(\\theta)$取得最大值对应的自变量 $\\pi_\\theta$ 是有一个固定公式的，即 $$\\large \\pi_r(y|x) = \\pi^*(y|x) = \\frac{1}{Z(x)}\\pi_{\\theta_{\\text{old}}}(y|x) \\exp\\left(\\frac{1}{\\beta}r(x,y)\\right)$$。 这里可以极简的方法推导处理，如下 $$L =\\mathbb{E}[r]-KL = \\sum_{p} p*(r-\\log(\\frac{p}{q})) = \\sum_{p} p*(\\log(e^r)-\\log(\\frac{p}{q}))=\\sum_{p} p*(\\log(\\frac{e^rq}{p})) = - \\sum_{p} p(\\log(\\frac{e^rq}{p})) = - KL(p, e^rq)$$，而最后一个式子就是KL散度的公式取负数，KL散度公式存在最小值，那么$L$就存在最大值，最优点即为 $\\large p = e^r*q$，最后公式外围套个归一化就是最终的推理结果。 直觉化的理解就是 策略函数 -\u0026gt; 和奖励结构对应的最优策略函数 -\u0026gt; 得到最优的整体目标值。而奖励结构必须与真实的正负样例偏好结构一致，所以整体上就是 奖励结构和策略函数是绑死的有固定函数关系，而奖励结构通过损失函数与真实正负样例偏好对齐，那么反向来说就是 真实正负样例偏好-\u0026gt;指导奖励结构计算 -\u0026gt;指导对应的策略函数， 而对应策略函数其实本来就应该达到一种极端策略（即使得奖励最大的response的概率直接拉到最大值1），但因为正则化的存在使得 策略函数是介于 老策略函数和 极端策略 之间的中间策略。 假如没有正则化，那么整个过程就是极端策略，让正样例的生成概率无限大，负样例的生成概率无限小，也就是对应的logits之差无限大，那么正样例的概率直接为1，负样例的概率直接为0，但这样其实就是过拟合。所以需要正则化来限制，那么就是通过 用概率的logits来表示 $r(x,y)$，同时假定损失结构是 让 $r(x,y)$ 和 KL散度直接相加，来作为最终的目标函数。 直观上理解DPO的公式，即让正偏好的对应的策略action概率越大好，让负偏好对应的策略action概率越小越好。 $$\\large L(\\theta)=-\\mathbb{E}{(x,y^+,y^-) \\sim \\mathcal{D}}\\left[\\log \\sigma\\left(\\beta \\log \\frac{\\pi{\\theta}(y^+|x)}{\\pi_{\\theta_{old}}(y^+|x)}-\\beta \\log \\frac{\\pi_{\\theta}(y^-|x)}{\\pi_{\\theta_{old}}(y^-|x)}\\right)\\right].$$ 其中$\\large \\hat{r}\\theta(x,y) = \\beta \\log \\left( \\frac{\\pi\\theta(y|x)}{\\pi_{\\theta_{old}}(y|x)} \\right)$ 意思是，如果当前策略对应的预估奖励是多少。\n直觉上我将DPO比作是一个三个铁杆上分别套着一个环，三个铁环之间 有两个绳子连接，传统的policy gradient是将通过拉最下面一个环，让上面两个间接连带着移动。而DPO是将上面两个环固化成一体，只要移动最下面一个，就能达到直接移动最上面铁环的目的。\nDPO算法的训练标准数据，都是配对的。\nDPO算法中损失函数中的logp指的是不是整条episode中答案的概率log，即在给定prompt的情况下，给出response的每个token概率的乘积，也就是logp的和。\nGRPO DeepSeekMath 论文的解读 [[2025Q3-论文学习日记#2025-07-16 DeepSeekMath]] GRPO 中使用了process supervision 信号，即单个reponse的多步推理，每一步都有标注的reward。然后 advantage 就是 归一化后的reward 在t步之后的累计和。即基于同一个prompt的所有response的reward的均值和方差 进行标准化 $\\Large \\tilde{r}i^{index(j)} = \\frac{r_i^{index(j)} - \\text{mean}(\\mathbf{R})}{\\text{std}(\\mathbf{R})}$ ，然后累计和 $\\Large \\hat{A}{i,t} = \\sum_{index(j) \\geq t} \\tilde{r}_i^{index(j)}$ ，这里t就是每一个token，j是每一个reasoning step，即每一个token其reward就是该token之后所有对应于 reasoning step end token的token的奖励之和。我想这里之所以没有使用$\\gamma$ 可能是因为reasoning step数量本来都是极其有限的，并且采样应该会控制其数量。 所以GRPO是需要使用 process reward model 对response的每一个推理步骤进行打分的。 The training data of the reward model is based on the rule judgment. Data Source: question in SFT dataset with outputs sampled from SFT model. Reward Function: Rule (whether the answer is correct or not) 论文里的公式里有很多讲究。\n原始论文中的公式19\n$\\large \\mathcal{J}{\\text{GRPO}}(\\theta) = \\mathbb{E}{\\mathbf{q} \\sim P_{\\text{sft}}(Q), {o_i}{i=1}^G \\sim \\pi{\\theta_{old}}(O|q)} \\large \\frac{1}{G} \\sum_{i=1}^G \\frac{1}{|o_i|} \\sum_{t=1}^{|o_i|} \\left[ \\frac{\\pi_\\theta(o_{i,t}|q, o_{i,\u0026lt;t})}{\\pi_{\\theta_{old}}(o_{i,t}|q, o_{i,\u0026lt;t})} \\hat{A}{i,t} - \\beta \\left( \\frac{\\pi{ref}(o_{i,t}|q, o_{i,\u0026lt;t})}{\\pi_\\theta(o_{i,t}|q, o_{i,\u0026lt;t})} - \\log \\frac{\\pi_{ref}(o_{i,t}|q, o_{i,\u0026lt;t})}{\\pi_\\theta(o_{i,t}|q, o_{i,\u0026lt;t})} - 1 \\right) \\right].$\n$\\large w_{i,t}(\\theta) = \\frac{\\pi_\\theta(y_{i,t}|x, y_{i,\u0026lt;t})}{\\pi_{\\theta_{\\text{old}}}(y_{i,t}|x, y_{i,\u0026lt;t})}$\nGSPO GSPO Group Sequence Policy Optimization\n$\\large J_{\\text{GSPO}}(\\theta) = \\mathbb{E}{x \\sim D, {y_i}{i=1}^G \\sim \\pi_{\\theta_{\\text{old}}}(\\cdot|x)} \\left[ \\frac{1}{G}\\sum_{i=1}^G \\min\\left(s_i(\\theta)\\hat{A}_i, \\text{clip}(s_i(\\theta), 1-\\epsilon, 1+\\epsilon)\\hat{A}_i\\right) \\right]$\n$\\large \\hat{A}i = \\frac{r(x, y_i) - \\text{mean}({r(x, y_j)}{j=1}^G)}{\\text{std}({r(x, y_j)}_{j=1}^G)}$\n$\\large s_i(\\theta) = \\left(\\frac{\\pi_\\theta(y_i|x)}{\\pi_{\\theta_{\\text{old}}}(y_i|x)}\\right)^{\\frac{1}{|y_i|}} = \\exp\\left(\\frac{1}{|y_i|}\\sum_{t=1}^{|y_i|}\\log\\frac{\\pi_\\theta(y_{i,t}|x,y_{i,\u0026lt;t})}{\\pi_{\\theta_{\\text{old}}}(y_{i,t}|x,y_{i,\u0026lt;t})}\\right)$\nGSPO和GRPO强化学习算法的区别\nGRPO GSPO 训练数据 process supervision outcome supervision 重要性采样 基于每一个token 基于整个序列，所有token重要性的的几何平均 ","permalink":"https://chesterwang.github.io/chester-blog/posts/2025-08-22-huggingface-%E7%B1%BB%E5%BA%93%E5%AD%A6%E4%B9%A0/","summary":"Hugging Face Hub Models, Spaces, and Datasets are hosted on the Hugging Face Hub as Git repositories Do you have files larger than 10MB? Those files should be tracked with git-lfs, which you can initialize with: git lfs install Note that if your files are larger than 5GB you’ll also need to run: hf lfs-enable-largefiles . Pull Request 之所以叫这个名字，是因为它准确地描述了请求者（你）和被请求者（项目维护者）之间的动作和方向。 PR = “我改好了，请你把我这边的修改拉（pull）进你的主分支吧","title":"Huggingface库学习笔记"},{"content":"\n🏡 关于本站 本博客记录个人的感想和开发过程。\n👦🏻 博主是谁 AI算法工程师\n🏹 兴趣爱好 🏃‍♂️跑步 | 🎧 音乐 | 🛌 摆烂\n","permalink":"https://chesterwang.github.io/chester-blog/about/","summary":"🏡 关于本站 本博客记录个人的感想和开发过程。 👦🏻 博主是谁 AI算法工程师 🏹 兴趣爱好 🏃‍♂️跑步 | 🎧 音乐 | 🛌 摆烂","title":"🙋🏻‍♂️ 关于"},{"content":"👉友链为随机顺序 cuikx\u0026#39;s blog cuikx\u0026#39;s blog Sulv\u0026#39;s Blog 一个记录技术、阅读、生活的博客 陈桂林博客 成功最有效的方法就是向有经验的人学习！ 黄忠德的博客 DevOps,SRE,Python,Golang程序员,开源爱好者 阿虚同学的储物间 收集了很多实用网站 老生杂谈的 IT 人 老生杂谈，后继有人。 Yunyi’s Blog Little squirrel Hopping around Yuin’s blog The world is your oyster 👉友链格式 名称: lvbibir's Blog\n网址: https://www.lvbibir.cn\n图标: https://www.lvbibir.cn/https://image.lvbibir.cn/blog/avatar.webp\n描述: life is a fucking movie\n👉友链申请要求 秉承互换友链原则、文章定期更新、不能有太多广告\n","permalink":"https://chesterwang.github.io/chester-blog/links/","summary":"👉友链为随机顺序 cuikx\u0026#39;s blog cuikx\u0026#39;s blog Sulv\u0026#39;s Blog 一个记录技术、阅读、生活的博客 陈桂林博客 成功最有效的方法就是向有经验的人学习！ 黄忠德的博客 DevOps,SRE,Python,Golang程序员,开源爱好者 阿虚同学的储物间 收集了很多实用网站 老生杂谈的 IT 人 老生杂谈，后继有人。 Yunyi’s Blog Little squirrel Hopping around Yu","title":"🤝 友链"},{"content":" ","permalink":"https://chesterwang.github.io/chester-blog/talk/","summary":"","title":"💬 说说"}]