跳到主要内容

🪄 Filter 函数:修改输入和输出

⚠️ 重要安全警告

Filter 函数会在您的服务器上执行任意 Python 代码。 函数的创建权限仅限于管理员。请仅安装来自信任源的函数,并在导入前仔细审核代码。恶意的 Function 可能会访问您的文件系统、窃取数据或破坏您的整个系统。有关完整详情,请参阅 Plugin 安全警告

欢迎阅读 Open WebUI 中的 Filter 函数完整指南!Filters 是一个灵活且强大的插件系统,用于在数据发送给大语言模型(LLM)之前(输入)或在数据从 LLM 返回之后(输出)对其进行修改。无论是为了获得更好的上下文而转换输入,还是为了提高可读性而清理输出,Filter 函数都能帮您轻松实现。

本指南将详细剖析 什么是 Filters、它们是如何工作的、它们的结构,以及您自己构建强大且用户友好的 filters 所需了解的一切。让我们深入探讨吧!我将使用比喻、示例和提示,让一切变得简单明了! 🌟


🌊 什么是 Open WebUI 中的 Filters?

将 Open WebUI 想象为流经管道的水流

  • 用户输入LLM 输出就是水。
  • Filters则是水处理阶段,在水到达最终目的地之前对其进行净化、修改和调整。

Filters 位于数据流的中间位置 — 就像检查站一样 — 您可以在这里决定需要调整什么。

以下是 Filters 所做工作的简要总结:

  1. 修改用户输入(Inlet 函数):在输入数据到达 AI 模型之前对其进行微调。您可以在这里增强文本的清晰度、添加上下文、净化文本,或重新格式化消息以满足特定要求。
  2. 拦截模型输出(Stream 函数):在 AI 响应生成时捕获并调整它们。这对于实时修改非常有用,例如过滤敏感信息或格式化输出以提高可读性。
  3. 修改模型输出(Outlet 函数):在 AI 的响应处理完成之后、展示给用户之前对其进行调整。这有助于精细化、记录或调整数据,以提供更整洁的用户体验。

核心概念:Filters 不是独立的模型,而是在数据往返模型的过程中,对其进行增强或转换的工具。

Filters 就像 AI 工作流中的翻译器或编辑:您可以拦截并改变对话,而不会中断工作流。


🗺️ Filter 函数的结构:骨架

让我们从 Filter 函数最简单的表示形式开始。如果某些部分一开始听起来很专业,请不要担心 — 我们会一步一步进行拆解!

🦴 Filter 的基本骨架

from pydantic import BaseModel
from typing import Optional

class Filter:
    # Valves: Filter 的配置选项
    class Valves(BaseModel):
        pass

    def __init__(self):
        # 初始化 valves(Filter 的可选配置)
        self.valves = self.Valves()

    async def inlet(self, body: dict) -> dict:
        # 在这里操纵用户输入。
        print(f"inlet called: {body}")
        return body

    async def stream(self, event: dict) -> dict:
        # 在这里修改模型输出的流式块(chunks)。
        print(f"stream event: {event}")
        return event

    async def outlet(self, body: dict) -> dict:
        # 在这里操纵模型输出。
        print(f"outlet called: {body}")
        return body

🧲 可切换的 Filters:让用户控制 Filter(self.toggle)

默认情况下,一个处于激活状态且在适用范围内(全局启用,或附加到当前模型)的 filter 会在每次请求时运行 — 用户对此无法干预。这通常是符合预期的(如 PII 敏感信息脱敏、记录日志、强制性的安全防护栏)。但有时您可能需要相反的效果:让用户自己决定是否为给定的对话启用该 filter。

设置 self.toggle = True 可以使 filter 变为用户可控的。接着,该 filter 会作为可点击的徽章(chip)显示在聊天 UI 中,并在“集成”菜单中显示一个条目,且仅在用户选择启用该 filter 的请求中运行。

from pydantic import BaseModel, Field
from typing import Optional

class Filter:
    class Valves(BaseModel):
        pass

    def __init__(self):
        self.valves = self.Valves()
        self.toggle = True   # 让该 filter 变为用户可控的(见下文说明)
        # 提示:为图标使用托管的 URL 而非 base64,以避免 API 响应体过于庞大。
        # 详情请参阅 Action 函数文档中关于为什么不推荐 base64 图标的部分。
        self.icon = "https://example.com/icons/lightbulb.svg"

    async def inlet(
        self, body: dict, __event_emitter__, __user__: Optional[dict] = None
    ) -> dict:
        # 此方法仅在用户当前选择了该 filter 时运行。
        # 您不需要在此处根据 self.toggle 编写分支逻辑 — 请参阅下方说明。
        await __event_emitter__(
            {
                "type": "status",
                "data": {"description": "运行中!", "done": True, "hidden": False},
            }
        )
        return body

self.toggle = True 实际在做什么

它是一个可见性 / 准入标志,在请求分发时被读取一次 — 而不是 UI 在您的 Python 对象上翻转的运行时状态。具体而言:

  • 可见性: 只有当 self.toggle = True 它是全局 filter 或附加到所选模型时,该 filter 才会显示在聊天 UI 中(内联徽章 + “集成”菜单条目)。如果没有设置 self.toggle,该 filter 仍会运行(如果处于激活状态且在适用范围内),但没有 UI 交互面 — 用户无法将其关闭。
  • 准入: 在请求时,后端会检查用户当前选择的 filter_ids 列表。如果该 filter 的 ID 在该列表中,inlet() / stream() / outlet() 就会运行。如果不在,该 filter 根本不会被调用
  • self.toggle 绝不会被 UI 更改。inlet() 内部,它始终是您在 __init__ 中设置的的值 — 在每一个真正运行的调用中,它都为 True,因为如果用户禁用了该 filter,inlet() 根本不会运行。不要编写在运行时读取 self.toggle 的逻辑;它不是一个实时的开关信号。
从 pre-0.9.0 版本的 filters 升级

一些旧版本的 filters 在 inlet() 中使用类似于 if self.toggle: enable_feature() else: disable_feature() 的模式,希望能通过这样在每次请求时读回 UI 状态。该模式从未可靠过,并且在 0.9.0+ 版本中已彻底失效。 当在 UI 中禁用该 filter 时,inlet() 根本不会被调用,因此没有 "else" 分支会执行。正确的迁移方案是完全停止根据 self.toggle 进行分支,只需无条件地执行具体的工作 — 用户通过选择/取消选择徽章来控制 inlet() 是否运行。如果您需要用户驱动的配置(例如数值阈值、目标语言等),请改通过 UserValves 暴露出来;点击徽章会自动打开用户的 valves 弹窗。

用户如何与可切换的 Filter 交互

当可切换的 filter 在当前聊天范围内时,会出现两个 UI 交互面:

  • 聊天输入栏中的内联徽章(chip)。 显示 filter 的 self.icon + 名称。点击它:
    • 如果该 filter 定义了 UserValves 类,则会打开 user-valves 弹窗(以便用户调整每场聊天的配置),否则
    • 会从当前的选择中移除该 filter(该徽章会从聊天输入栏的行中消失)。
  • “集成”菜单(⚙️ 图标)。 列出当前范围内的每一个可切换 filter,每个 filter 都带有一个对应的开关(Switch)。用户可以在这里重新启用他们从徽章栏中移除的 filter,或者关闭默认选中的 filter。

徽章存在 = 该 filter 已为下一次请求启用。徽章不存在(但该 filter 在“集成”菜单中) = 用户已将其关闭。

选择状态保存在哪里

  • 作为 sessionStorage 草稿状态保存在浏览器中,每场聊天都有独立的键值。
  • 在同一浏览器会话中刷新页面后依然存在,但不会被持久化到服务器端的聊天记录中
  • 初始状态来自模型的 defaultFilterIds(管理员面板 → 模型设置 → 默认 Filters) — 管理员可以决定每个模型中哪些可切换的 filters 默认处于开启关闭状态。
  • 当用户切换到其他模型时,选择状态会重置。

切换 Filter

self.icon 的工作方式与之前一致:传入一个 URL(强烈推荐)或 base64 数据 URI,它就会渲染在内联徽章和“集成”菜单条目中。关于为什么推荐托管的 URL 而非 base64,请参见 Action 函数 icon_url 警告


⚙️ Filter 管理与配置

🌐 全局 Filters vs. 特定于模型的 Filters

Open WebUI 提供了一个灵活的多层级 filter 系统,允许您控制哪些 filters 处于激活状态、它们是如何启用的,以及谁可以切换它们。理解这个系统对于高效管理 filters 至关重要。

Filter 激活状态

Filters 在数据库中通过两个布尔标志控制,可能存在以下四种状态之一:

状态is_activeis_global效果
全局启用TrueTrue自动应用到所有模型,无法在单个模型中禁用
全局禁用FalseTrue不会应用到任何地方 — 即便该 filter 被全局启用,但 filter 本身被关闭了
特定于模型TrueFalse仅应用到管理员显式启用了它的模型上
未激活FalseFalse不会应用到任何地方,即使管理员在某个模型中启用了它 — 该 filter 本身已被关闭
全局 Filter 行为

当一个 filter 被设置为全局is_global=True)且激活is_active=True)时,它会被强制启用到所有模型上:

  • 它会出现在每个模型的 filter 列表中,显示为已勾选且置灰(不可操作)
  • 管理员无法在模型设置中取消勾选它
  • 无论使用什么模型,它都会在每一次聊天生成请求中运行

管理面板:使 Filter 全局生效

位置: 管理员面板 → Functions → Filter 管理

要使 filter 全局生效:

  1. 导航到管理员面板
  2. 点击侧边栏中的 Functions
  3. 在列表中找到您的 filter
  4. 点击 filter 旁的三点菜单 (⋮)
  5. 点击🌐 地球图标以切换 is_global 状态
  6. 确保该 filter 处于激活状态(绿色切换开关)

API 端点:

POST /functions/id/{filter_id}/toggle/global

视觉指示器:

  • 🟢 绿色开关 = is_active=True(filter 处于激活状态)
  • 🌐 高亮显示的地球图标 = is_global=True(应用到所有模型)

🎛️ 双层 Filter 系统

Open WebUI 使用了一套精致的双层系统来管理单个模型上的 filters。这在一开始可能会让人有些困惑,但它的设计是为了同时支持常开 filters用户可切换 filters

第一层:FiltersSelector(哪些 Filter 可用?)

位置: 模型设置 → Filters → "Filters" 区域

这控制了哪些 filters 对特定的模型可用

行为:

  • 显示所有 filters(包括全局的和特定于模型的)
  • 全局 filters 显示为已勾选且置灰(无法取消勾选)
  • 常规 filters 可以手动切换开启/关闭
  • 在数据库中保存为:model.meta.filterIds

示例:

{
  "meta": {
    "filterIds": ["filter-uuid-1", "filter-uuid-2"]
  }
}

第二层:DefaultFiltersSelector(哪些可切换 Filter 默认启用?)

位置: 模型设置 → Filters → "Default Filters" 区域

此区域仅在至少选择了一个可切换 filter(或存在全局 filter)时才会出现。

目的: 控制哪些可切换的 filters 在新聊天中默认处于启用状态

什么是“可切换” Filter?

当 Filter 的 Python 代码包含以下内容时,它就会变成可切换的:

class Filter:
    def __init__(self):
        self.toggle = True  # 这会让它变成可切换的!

行为:

  • 仅显示带有 toggle=True 的 filters
  • 仅显示符合以下任一条件的 filters:
    • filterIds 中(为该模型选中),或者
    • 拥有 is_global=true(全局启用)
  • 控制该 filter 在聊天 UI 中默认是处于 ON 还是 OFF 状态
  • 在数据库中保存为:model.meta.defaultFilterIds

示例:

{
  "meta": {
    "filterIds": ["filter-uuid-1", "filter-uuid-2", "filter-uuid-3"],
    "defaultFilterIds": ["filter-uuid-2"]
  }
}

解读:

  • 这三个 filters 对该模型都可用
  • 只有 filter-uuid-2 在初始时默认启用
  • 如果 filter-uuid-1filter-uuid-3 拥有 toggle=True,用户可以在聊天 UI 中手动启用它们

🔄 可切换 Filters vs. 常开 Filters

理解这两者之间的区别是有效使用 filter 系统的关键。

常开 Filters(无 toggle 属性)

特点:

  • 只要在模型中处于激活状态,就会自动运行
  • 用户在聊天界面无法控制
  • 不会出现在 "Default Filters" 区域
  • 不会出现在聊天“集成”菜单(⚙️ 图标)中

使用场景:

  • 内容审核 - 过滤脏话、仇恨言论或不当内容
  • PII 脱敏 - 帮助脱敏电子邮件、电话号码、身份证号、信用卡号
  • 提示词注入检测 - 阻止试图操纵系统提示词的企图
  • 输入/输出日志记录 - 记录所有对话以便审计或分析
  • 成本追踪 - 估算并记录 token 使用量以进行计费
  • 速率限制 - 强制实施针对每个用户或全局的请求限制
  • 语言强制 - 确保响应采用特定的语言
  • 公司政策执行 - 注入法律免责声明或合规通知
  • 模型路由 - 根据内容将请求重定向到不同的模型

示例:

class ContentModerationFilter:
    def __init__(self):
        # 没有 toggle 属性 — 这是一个常开 filter
        pass

    async def inlet(self, body: dict) -> dict:
        # 在发送给模型之前始终对 PII 进行脱敏
        last_message = body["messages"][-1]["content"]
        body["messages"][-1]["content"] = self.scrub_pii(last_message)
        return body

可切换 Filters(toggle=True)

特点:

  • 聊天输入栏中显示为可点击的徽章,在**“集成”菜单(⚙️ 图标)中显示为开关**。
  • 用户可以在每场聊天、每个会话中,将它们从当前选中的 filter 中添加或移除。选择状态保存在浏览器的 sessionStorage 中 — 不会持久化到服务器端的聊天记录中。
  • 出现在模型的 "Default Filters" 配置中。
  • 模型上的 defaultFilterIds 控制初始选择(当使用该模型开始新聊天时,哪些可切换 filters 处于开启状态)。
  • self.toggle 本身在运行时绝不会被改变 — 它是一个在请求分发时被读取一次的可见性/准入标志。inlet() 仅在 filter 当前被选中时运行;您不需要在 filter 内部编写 "else" 分支。参见上方的详细说明

使用场景:

  • 联网搜索集成 - 用户决定何时在联网搜索中获取上下文
  • 引用模式 - 用户控制何时在响应中强制要求提供来源
  • 详细/精简模式 - 用户在精简响应与详细响应之间切换
  • 翻译 filters - 用户启用与特定语言之间的互相翻译
  • 代码格式化 - 用户选择何时应用语法高亮或 lint 检查
  • 思考/推理切换 - 用户通过启用该 filter,切换底层模型的思考模式(在 inlet() 中无条件执行具体工作;用户通过移除徽章来禁用它)
  • Markdown 渲染 - 在原始文本与格式化输出之间切换
  • 匿名模式 - 用户在讨论敏感话题时启用
  • 专家模式 - 注入特定领域的上下文(法律、医疗、技术)
  • 创意写作模式 - 针对创意任务调整温度和写作风格

示例:

class WebSearchFilter:
    def __init__(self):
        self.toggle = True  # 允许用户控制
        self.icon = "https://example.com/icons/web-search.svg"

    async def inlet(self, body: dict, __event_emitter__) -> dict:
        # 这只在用户选择了该 filter 时运行。
        # 切勿在此处根据 self.toggle 进行分支 — 运行到这里时它必然为 True。
        await __event_emitter__({
            "type": "status",
            "data": {"description": "正在联网搜索...", "done": False}
        })
        # ... 执行联网搜索 ...
        return body

可切换 Filters 出现的位置:

  1. 模型设置 → 默认 Filters
    • 管理员选择哪些可切换 filters 在使用该模型的新聊天中初始时处于勾选状态。
  2. 聊天输入栏 → 内联徽章 (chip)
    • 为用户当前在这场聊天中选中的每一个可切换 filter 显示。
    • 点击徽章:如果该 filter 定义了 UserValves,则会打开 user-valves 弹窗,否则会将该 filter 从当前的选择中移除(它会移回“集成”菜单中,用户可以在那里重新启用它)。
    • self.icon 会渲染为徽章的图标。
  3. 聊天 UI → “集成”菜单(⚙️ 图标)
    • 列出当前模型范围内的每一个可切换 filter,每个 filter 都带有一个对应的开关 (Switch)
    • 用于重新启用从徽章栏移除的 filter,或者关闭默认选中的 filter。

📊 Filter 执行流程

以下是从管理员配置到 filter 执行的完整流程:

1. 管理员面板(Filter 创建与全局设置)

  • 管理员面板 → Functions → 创建新 Function
  • 设置类型为 "filter"
  • 切换 is_active(全局启用/禁用该 filter)
  • 切换 is_global(应用到所有模型)

2. 模型配置(在单个模型中选择 Filter)

  • 模型设置 → Filters 区域
  • FiltersSelector:选择该模型要使用哪些 filters
  • DefaultFiltersSelector:设置默认启用状态(仅针对可切换 filters)

3. 聊天 UI(用户交互 - 仅限可切换 Filters)

  • 聊天输入栏 → 内联徽章(点击移除;如果定义了 UserValves,点击会打开弹窗)
  • 聊天 → “集成”菜单 (⚙️) → 每个可切换 filter 旁的开关
  • 前端在 sessionStorage 中追踪 selectedFilterIds(每场聊天,每个会话独立)
  • 初始选择状态由模型的 defaultFilterIds 播种
  • 常开 filters(无 self.toggle)会自动运行,没有 UI 控制项

4. 请求处理(Filter 编译)

  • 前端将当前选中的 filter_ids 随请求一同发送
  • 后端执行:get_sorted_filter_ids(request, model, filter_ids)
  • 获取全局 filters(is_global=Trueis_active=True)+ 模型设置中 model.meta.filterIds 指定的特定于模型的 filters
  • 根据 is_active 状态进行过滤
  • 对于可切换 filters: 仅保留其 ID 在请求中 filter_ids 列表内的 filters — 其他 filters 会被完全丢弃(永远不会被调用,也不会读取其 self.toggle 属性)
  • 根据优先级进行排序(来自 valves)

5. FILTER 执行

  • 执行 inlet() 过滤器(请求前)
  • 将修改后的请求发送给 LLM
  • 执行 stream() 过滤器(流式传输期间)
  • 执行 outlet() 过滤器(响应后)

📡 API 请求中的 Filter 行为

当直接使用 Open WebUI 的 API 端点(例如,通过 curl 或外部应用程序)时,inlet()stream() 遵循与 WebUI 请求相同的执行模型。对于直接 API 调用者,outlet() 的行为会非常不同,我们将在下面详细介绍。

关键行为差异

函数WebUI 请求直接 API — 稳定版 (main)直接 API — 预览版 (dev)
inlet()✅ 总是被调用✅ 总是被调用✅ 总是被调用
stream()✅ 在流式传输期间被调用✅ 在流式传输期间被调用✅ 在流式传输期间被调用
outlet()✅ 在响应结束后被调用❌ 绝不会被 /api/chat/completions 调用 — 仅被 /api/chat/completed 调用⚠️ 仅在极其狭窄的条件下内联运行(见下文)
__event_emitter__✅ 显示 UI 反馈⚠️ 对纯 API 调用者无效(不起作用)⚠️ 对纯 API 调用者无效(不起作用)
针对直接 API 调用的 outlet() — 真实情况

先前版本的此页面声称,对于非流式直接 API 请求,outlet() 会在 /api/chat/completions 期间内联运行。这只是部分准确,且仅适用于 dev 分支。根据后端源码验证,实际行为是:

在已发布的稳定版本 / main 上: outlet() 绝不会/api/chat/completions 调用。它只有在调用者向 /api/chat/completed 发起第二次 POST 请求时才会运行。需要使用 outlet() 的 API 集成必须同时发起这两个调用。

dev / 预发布构建上: outlet() 可以在 /api/chat/completions 后内联触发,但仅在以下所有条件均满足时才起作用:

  1. 请求体必须同时包含 chat_id id(助手消息的 ID)。如果缺少其中任何一个,后端会认为 event_emitter = None 并静默跳过 outlet 块。
  2. chat_id 必须是已认证用户已拥有的聊天;否则请求会在运行 outlet 路径之前返回 404 错误。(或者,发送 parent_id: null 且不带 chat_id 以让服务器创建新聊天。)
  3. 请求必须是非流式的。在流式路径上,服务器自身会消耗流并把内容路由到用户的 WebSocket — HTTP 流式 API 调用者实际上接收不到任何东西。outlet() 仍会在数据库端运行,但流式客户端看不到它。

即使在非流式路径上,outlet() 也不会重写 HTTP 响应体。处理器只会更新持久化的聊天消息行,并触发 chat:outlet WebSocket 事件;返回给您 HTTP 客户端的 JSON 仍是 outlet 处理前的内容。要观察 outlet() 的输出,您必须重新读取聊天记录、订阅 WebSocket,或者使用 /api/chat/completed

对于 filter 作者的结论: 如果您的 filter 的 outlet() 需要对纯 API 消费者(如 Continue.dev、Claude Code、Langfuse 追踪、自定义脚本)可见,请在目前将 /api/chat/completed 视为唯一受支持的交互面。依赖在 /api/chat/completions 期间内联执行 outlet() 目前仅适用于与 WebUI 结构相同的客户端。

通过 metadata 进行 Inlet ↔ Outlet 关联 {#inlet--outlet-correlation-via-metadata}

__metadata__ 是在请求生命周期中传递的实时字典,因此您的 filter 在 inlet() 中存入的任何内容,在同一个请求outlet() 中都是可见的 — 这对于计算耗时、追踪关联 ID 或每轮调用的 valves 非常有用(outlet() 真正运行的情况下)。鉴于上述限制,该模式主要适用于 WebUI 请求以及 /api/chat/completed 处理器中。

import time
from pydantic import BaseModel, Field

class Filter:
    class Valves(BaseModel):
        priority: int = Field(default=0)

    def __init__(self):
        self.valves = self.Valves()

    async def inlet(self, body: dict, __metadata__: dict = None) -> dict:
        if __metadata__ is not None:
            __metadata__["_my_started_at"] = time.monotonic()
        return body

    async def outlet(self, body: dict, __metadata__: dict = None) -> dict:
        if __metadata__ is not None and "_my_started_at" in __metadata__:
            duration = time.monotonic() - __metadata__["_my_started_at"]
            print(f"请求耗时:{duration:.3f}秒")
        return body
在 inlet() 中合成 chat_id / message_id 并不是可靠的规避方案

一些旧版本的此页面建议在 inlet() 内部合成一个 local:<uuid>chat_id 和随机的 message_id,以强制让 outlet() 为纯 API 调用者运行。请不要依赖此模式 — 在当前的后端代码中,聊天所有权检查在 filter 管道之前运行,并且即便内联 outlet 处理器确实运行了,它也不会重写 HTTP 响应体。如果您需要通过 HTTP 为 API 调用者获取 outlet() 的输出,请改用 /api/chat/completed

为 API 调用者运行 outlet():/api/chat/completed

对于直接 API 集成,运行 outlet() 的可靠且受支持的方式是,在 /api/chat/completions 返回响应后,向 POST /api/chat/completed 发送请求,在 messages 中传入完整的对话内容(包括助手的响应)。该端点会无条件地运行管道中的 outlet 过滤器以及 Function 的 outlet() 处理器,并返回过滤后的数据负载。

# 第二步:在 /api/chat/completions 返回响应后运行 outlet()。
curl -X POST http://localhost:3000/api/chat/completed \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "llama3.1",
    "messages": [
      {"role": "user", "content": "Hello"},
      {"role": "assistant", "content": "Hi there! How can I help you?"}
    ],
    "chat_id": "optional-chat-id",
    "session_id": "optional-session-id"
  }'
说明

dev 分支上,该端点被标记为已弃用(推荐内联执行),但由于内联执行不会通过 HTTP 向纯 API 调用者返回过滤后的负载,因此 /api/chat/completed 在目前仍是大多数 API 集成的正确选择。

检测 API vs WebUI 请求

您可以通过检查 __metadata__ 参数来检测请求是源自 WebUI 还是直接 API 调用:

async def inlet(self, body: dict, __metadata__: dict = None) -> dict:
    # 检查请求是否来自 WebUI
    interface = __metadata__.get("interface") if __metadata__ else None
    
    if interface == "open-webui":
        print("来自 WebUI 的请求")
    else:
        print("直接 API 请求")
    
    # 您也可以检查是否存在聊天上下文
    chat_id = __metadata__.get("chat_id") if __metadata__ else None
    if not chat_id:
        print("无聊天上下文 — 可能是直接 API 调用")
    
    return body

示例:针对所有请求进行速率限制

由于 inlet() 总是会被调用,因此可以用它来实现同时适用于 WebUI 和 API 请求的速率限制:

from pydantic import BaseModel, Field
from typing import Optional
import time

class Filter:
    class Valves(BaseModel):
        requests_per_minute: int = Field(default=60, description="每个用户每分钟的最大请求数")
    
    def __init__(self):
        self.valves = self.Valves()
        self.user_requests = {}  # 追踪每个用户的请求
    
    async def inlet(self, body: dict, __user__: dict = None) -> dict:
        if not __user__:
            return body
        
        user_id = __user__.get("id")
        current_time = time.time()
        
        # 清理旧记录并计算近期请求数
        if user_id not in self.user_requests:
            self.user_requests[user_id] = []
        
        # 仅保留过去一分钟内的请求
        self.user_requests[user_id] = [
            t for t in self.user_requests[user_id] 
            if current_time - t < 60
        ]
        
        if len(self.user_requests[user_id]) >= self.valves.requests_per_minute:
            raise Exception(f"超出速率限制:每分钟最大 {self.valves.requests_per_minute} 次请求")
        
        self.user_requests[user_id].append(current_time)
        return body

示例:记录所有 API 使用情况

同时为 WebUI 和直接 API 调用追踪 token 使用情况及请求:

from pydantic import BaseModel, Field
from typing import Optional
import logging

class Filter:
    class Valves(BaseModel):
        log_level: str = Field(default="INFO", description="日志级别")
    
    def __init__(self):
        self.valves = self.Valves()
        self.logger = logging.getLogger("api_usage")
    
    async def inlet(self, body: dict, __user__: dict = None, __metadata__: dict = None) -> dict:
        user_email = __user__.get("email", "unknown") if __user__ else "anonymous"
        model = body.get("model", "unknown")
        interface = __metadata__.get("interface", "api") if __metadata__ else "api"
        chat_id = __metadata__.get("chat_id") if __metadata__ else None
        
        self.logger.info(
            f"Request: user={user_email}, model={model}, "
            f"interface={interface}, chat_id={chat_id or 'none'}"
        )
        
        return body
关于事件发射器的说明

在 API 请求中,使用 __event_emitter__ 的 filters 仍会照常执行,但由于没有 WebUI 来展示事件,因此状态消息将不可见。Filter 逻辑仍会运行 — 只是缺失了视觉反馈。


⚡ Filter 优先级与执行顺序

当有多个 filters 激活时,它们会以特定的顺序执行,该顺序由它们的**优先级(priority)**决定。在构建一个 filter 的更改依赖于另一个 filter 更改的 filter 链(filter chains)时,理解这一点至关重要。

设置 Filter 优先级

优先级是在 Valves 类中通过 priority 字段进行配置的:

class Filter:
    class Valves(BaseModel):
        priority: int = Field(
            default=0,
            description="Filter 执行顺序。较低的值会先运行。"
        )
    
    def __init__(self):
        self.valves = self.Valves()
    
    async def inlet(self, body: dict) -> dict:
        # 该 filter 的执行顺序取决于其 priority 值
        return body

优先级排序规则

优先级数值执行顺序
0 (默认)最先运行
1在优先级 0 之后运行
2在优先级 1 之后运行
较低优先级数值 = 较早执行

Filters 会根据优先级按升序进行排序。拥有 priority=0 的 filter 会在 priority=1 的 filter 之前运行,后者又在 priority=2 之前运行,依此类推。当多个 filters 共享相同的优先级数值时,它们会按 Function ID 字母顺序进行排序,以实现确定性的排序。


🔗 Filters 之间的数据传递

当有多个 filters 激活时,链条中的每个 filter 都会接收到前一个 filter 修改后的数据。一个 filter 返回的值会变成优先级顺序中下一个 filter 的输入。

用户输入

模型路由 Filter (priority=0) → 改变 body 的某些部分

上下文管理 Filter (priority=1) → 接收修改后的 body ✓

日志记录 Filter (priority=2) → 接收包含所有先前更改的 body ✓

LLM 请求(将最终修改后的 body 发送给 OpenAI/Ollama API)
重要提示:必须返回 Body

如果您的 filter 修改了 body,您必须将其返回。返回的值会被传递给下一个 filter。如果您返回了 None,后续的 filters 将会运行失败。

async def inlet(self, body: dict, __event_emitter__) -> dict:
    body["messages"].append({"role": "system", "content": "Hello"})
    return body  # 千万不要忘记这一步!

🔌 注入额外的 API 请求体参数

Inlet 过滤器可以在请求体中注入额外的字段,这些字段会被转发给外部的 LLM API。这对于 Open WebUI 在 UI 中没有暴露的、特定于 API 的参数非常有用。

请求体会从您的 inlet 过滤器流向 LLM API,而不会剥离未知的字段 — 只有像 metadatafeaturestool_idsfilesskill_ids 这样的内部键才会被移除。您添加的任何其他字段都将被序列化为 JSON 并发送给 API 提供商。

示例:OpenAI 安全标识符

OpenAI 建议随每次请求发送一个 safety_identifier 以进行滥用检测。您可以通过过滤器自动注入它:

import hashlib

class Filter:
    async def inlet(self, body: dict, __user__: dict = None) -> dict:
        if __user__ and __user__.get("id"):
            body["safety_identifier"] = hashlib.sha256(
                __user__["id"].encode()
            ).hexdigest()
        return body

脱敏后的用户 UUID 被添加为一个顶级的请求体参数,并直接转发给 OpenAI 的 API — 没有发送任何 PII 敏感数据,仅仅是一个不透明的哈希值。

无法注入 HTTP 请求头

过滤器只能修改请求体 (form_data)。外发的 HTTP 请求头是单独构建的,无法在过滤器中受到影响。要在 API 请求中添加自定义标头,请使用管理员面板 → 设置 → 连接 → OpenAI API 标头配置。

从 Filter 注入 OpenAI 风格的 tools

Filter inlet 还可以将 OpenAI 风格的函数调用工具(function-calling tools)追加到 body["tools"] 中。这些会与 Open WebUI 在服务器端根据 tool_ids、MCP 服务器以及模型的内置工具解析出来的 tools 进行合并 — 它们不会替换已有的 tools。

class Filter:
    async def inlet(self, body: dict) -> dict:
        body.setdefault("tools", []).append({
            "type": "function",
            "function": {
                "name": "lookup_user_tier",
                "description": "返回调用者的计费层级。",
                "parameters": {
                    "type": "object",
                    "properties": {"user_id": {"type": "string"}},
                    "required": ["user_id"],
                },
            },
        })
        return body

行为:

  • 原生函数调用(默认方式)。过滤器注入的 tools 会在发送聊天完成请求之前,被追加到服务器解析的 tools 列表末尾。两组 tools 对同一个请求中的模型都是可见的。
  • 由原始 API 调用者提供的 tools。 如果进入过滤器管道的请求本身就已经包含了 body["tools"](例如外部客户端在调用 /api/chat/completions 时自带了 tools 数组),这些调用者提供的 tools 会拥有更高的优先级,并且 Open WebUI 会完全跳过服务器端的 tools 解析。在这种情况下,Filter inlet 也应当采用追加的方式 — 调用者的 tools 和您的 tools 都会一同发往 LLM。
  • 非原生函数调用。 服务器端的 tools 解析仍会通过 Open WebUI 基于提示词的 tools 处理器运行;过滤器注入的 tools 会转发给 LLM,但它们的运行时执行器则取决于您在 upstream 调用中接入的任何内容。

对于没有在工作区注册对应 Tool 的情况,可以使用此方式 — 例如,您仅希望在特定用户的聊天中让模型知道某些临时的、每轮请求独立的交互能力。


🔍 解析底层模型(__model__) {#resolving-the-base-model-model}

当用户选择了一个工作区模型或自定义模型时,body["model"] 会包含该自定义模型 ID(例如 "my-custom-gpt5"),而不是底层的基座模型。要发现实际的基座模型,可以使用双下划线参数 __model__

class Filter:
    async def inlet(self, body: dict, __model__: dict = None) -> dict:
        custom_model_id = body["model"]  # 例如 "my-custom-gpt5"

        base_model_id = None
        if __model__ and "info" in __model__:
            base_model_id = __model__["info"].get("base_model_id")
            # 例如 "gpt-5.2"

        if base_model_id:
            print(f"工作区模型 '{custom_model_id}' → 基座模型 '{base_model_id}'")
        else:
            print(f"直接调用基座模型:'{custom_model_id}'")

        return body

如果不存在 base_model_id,则说明用户直接选择了一个基座模型(没有工作区包装层)。

可用的双下划线(Dunder)参数

Filters 可以在其函数签名中声明以下任何参数,以便自动接收它们:

参数提供的内容
__model__完整的模型字典(对于工作区模型,包含 info.base_model_id
__user__用户数据(id, email, name, role
__metadata__请求元数据(chat_id, session_id, interface 等)
__event_emitter__用于向客户端发送状态更新、嵌入内容等的函数
__chat_id__聊天会话 ID
__request__原始的 FastAPI Request 对象

只有在您的函数签名中声明了的参数才会被注入 — Open WebUI 在运行时会检查函数签名以决定要传递什么。


🎨 UI 指示器与视觉反馈

在管理员 Functions 面板中

指示器含义
🟢 绿色开关Filter 处于激活状态 (is_active=True)
⚪ 灰色开关Filter 处于未激活状态 (is_active=False)
🌐 高亮显示的地球Filter 属于全局有效 (is_global=True)
🌐 置灰的地球Filter 属于非全局有效 (is_global=False)

在模型设置中 (FiltersSelector)

状态复选框描述
全局 Filter✅ 已勾选 & 置灰"此过滤器已全局启用"
已选 Filter✅ 已勾选 & 可点击"此过滤器已为该模型选中"
未选 Filter☐ 未勾选 & 可点击"点击以引入该过滤器"

在聊天 UI 中(集成菜单)

元素描述
Filter 名称显示 filter 的展示名称
自定义图标来自 self.icon 的 SVG 图标(如果提供)
切换开关为当前聊天启用/禁用该 filter
状态徽章显示该 filter 当前是否处于活跃状态

💡 Filter 配置最佳实践

1. 何时使用全局 Filters

将全局 filters 用于:

  • 安全与合规性(PII 脱敏、内容审核)
  • 系统范围内的格式化(规范化所有输出)
  • 日志记录与分析(追踪所有请求)
  • 整个组织范围内的政策(执行公司指南)

不要将全局 filters 用于:

  • 可选的功能(改用可切换 filters)
  • 特定于模型的行为(使用特定于模型的 filters)
  • 用户偏好功能(通过开关让用户自行控制)

2. 何时使用可切换 Filters

在以下情况将 filter 设为可切换 (toggle=True):

  • 用户应当控制何时启用该 filter(如联网搜索、翻译)
  • 它是可选的增强功能(如引用模式、详细输出)
  • 它添加了用户可能不总是想要的功能(如代码格式化)
  • 它具有一定的性能成本,应当是可选的

不要在以下情况将 filter 设为可切换:

  • 它是安全/合规所必需的(常开是更好的选择)
  • 用户不应当能够禁用它(使用常开模式)
  • 它是系统层面的数据转换(全局启用是更好的选择)

3. 为您的组织组织 Filters

推荐结构:

全局常开 Filters:
├─ PII 脱敏器 (安全)
├─ 内容审核器 (合规)
└─ 请求记录器 (分析)

特定于模型的常开 Filters:
├─ 代码格式化器 (仅针对编程模型)
├─ 医学术语纠错器 (仅针对医学模型)
└─ 法律引用验证器 (仅针对法律模型)

可切换的 Filters (用户自主选择):
├─ 联网搜索集成
├─ 引用模式
├─ 翻译过滤器
├─ 详细输出模式
└─ 图像描述生成器

🎯 核心组件解析

1️⃣ Valves 类(可选设置)

Valves 想象为您的 filter 上的旋钮和滑块。如果您想提供可配置的选项来调整您的 Filter 的行为,您可以在这里进行定义。

class Valves(BaseModel):
    OPTION_NAME: str = "Default Value"

例如: 如果您正在创建一个将响应转换为大写的 filter,您可以通过一个类似于 TRANSFORM_UPPERCASE: bool = True/False 的 valve,允许用户配置是否将所有输出内容都完全大写。

使用下拉菜单配置 Valves(枚举 Enums)

您可以通过为某些 Valves 提供下拉菜单而非自由文本输入,来提升用户配置 filter 设置时的体验。这可以通过在您的 Pydantic Field 定义中结合使用 json_schema_extraenum 关键字来实现。

enum 关键字允许您指定一组预定义的值,UI 应当将这些值渲染为下拉菜单中的可选项。

示例: 在 filter 中为颜色主题创建一个下拉菜单。

from pydantic import BaseModel, Field
from typing import Optional

# 定义您的可用选项(例如,颜色主题)
COLOR_THEMES = {
    "Plain (No Color)": [],
    "Monochromatic Blue": ["blue", "RoyalBlue", "SteelBlue", "LightSteelBlue"],
    "Warm & Energetic": ["orange", "red", "magenta", "DarkOrange"],
    "Cool & Calm": ["cyan", "blue", "green", "Teal", "CadetBlue"],
    "Forest & Earth": ["green", "DarkGreen", "LimeGreen", "OliveGreen"],
    "Mystical Purple": ["purple", "DarkOrchid", "MediumPurple", "Lavender"],
    "Grayscale": ["gray", "DarkGray", "LightGray"],
    "Rainbow Fun": [
        "red",
        "orange",
        "yellow",
        "green",
        "blue",
        "indigo",
        "violet",
    ],
    "Ocean Breeze": ["blue", "cyan", "LightCyan", "DarkTurquoise"],
    "Sunset Glow": ["DarkRed", "DarkOrange", "Orange", "gold"],
    "Custom Sequence (See Code)": [],
}

class Filter:
    class Valves(BaseModel):
        selected_theme: str = Field(
            "Monochromatic Blue",
            description="为 LLM 响应选择一个预定义的颜色主题。'Plain (No Color)' 禁用着色。",
            json_schema_extra={"enum": list(COLOR_THEMES.keys())}, # 关键点:这会创建下拉菜单
        )
        custom_colors_csv: str = Field(
            "",
            description="'Custom Sequence' 主题的 CSV 格式颜色(例如 'red,blue,green')。使用 xcolor 命名规范。",
        )
        strip_existing_latex: bool = Field(
            True,
            description="如果为 true,尝试移除已有的 LaTeX 颜色命令。推荐以避免嵌套渲染问题。",
        )
        colorize_type: str = Field(
            "sequential_word",
            description="如何应用颜色:'sequential_word'(逐字)、'sequential_line'(逐行)、'per_letter'(逐字母)、'full_message'(整条消息)。",
            json_schema_extra={
                "enum": [
                    "sequential_word",
                    "sequential_line",
                    "per_letter",
                    "full_message",
                ]
            }, # 另一个 enum 下拉菜单的示例
        )
        color_cycle_reset_per_message: bool = Field(
            True,
            description="如果为 true,每条新的 LLM 响应消息的颜色序列都会重新开始。如果为 false,它会在多条消息间延续。",
        )
        debug_logging: bool = Field(
            False,
            description="在控制台中启用详细的日志记录,以便调试过滤操作。",
        )

    def __init__(self):
        self.valves = self.Valves()
        # ... 您的其他 __init__ 逻辑 ...

发生了什么?

  • json_schema_extraField 中的这个参数允许您注入 Pydantic 并不直接支持的、任意的 JSON Schema 属性,但这些属性可以被下游工具(如 Open WebUI 的 UI 渲染器)所使用。
  • "enum": list(COLOR_THEMES.keys()):这告诉 Open WebUI,selected_theme 字段应当呈现一组值的选择,具体是我们的 COLOR_THEMES 字典的键。接着,UI 会渲染一个下拉菜单,其中包含 "Plain (No Color)", "Monochromatic Blue", "Warm & Energetic" 等可选项。
  • colorize_type 字段也展示了针对不同着色方法的另一个 enum 下拉菜单。

为您的 Valves 选项使用 enum 可以使您的 filters 更加用户友好,并能阻止无效的输入,从而带来更顺畅的配置体验。


2️⃣ inlet 函数(输入前置处理)

inlet 函数就像是烹饪前的食材准备。想象您是一位厨师:在食材进入菜谱(在此处即为 LLM)之前,您可能会洗菜、切洋葱或腌制肉类。没有这一步,您的最终菜肴可能缺乏风味、存有未清洗的农产品,或者不够协调。

在 Open WebUI 的世界中,inlet 函数会在用户输入发送给模型之前,对其进行这些重要的准备工作。它确保输入对于 AI 处理而言,是尽可能整洁、具有背景上下文且有所助益的。

📥 输入

  • body:Open WebUI 发送给模型的原始输入。它是聊天完成请求的格式(通常是一个包含对话消息、模型设置和其他元数据的字典)。可以将这视作您的菜谱食材。

🚀 您的任务: 修改并返回 body。修改后的 body 版本才是 LLM 真正开始工作时所拿到的数据,因此这是您为输入带来清晰度、结构和背景上下文的绝佳机会。

🍳 为什么使用 inlet?
  1. 添加上下文:自动在用户输入中追加关键信息,特别是在用户文本模糊或不完整时。例如,您可以添加“您是一位友好的助手”或“帮助此用户排查软件 bug”。
  2. 格式化数据:如果输入需要特定的格式(如 JSON 或 Markdown),您可以在将数据发送给模型前进行转换。
  3. 净化输入:移除不需要的字符,剥离潜在有害或令人困惑的符号(如多余的空格或表情符号),或脱敏敏感信息。
  4. 精简用户输入:如果附加指导能显著改善您的模型输出,您可以使用 inlet 自动注入澄清说明!
  5. 速率限制:追踪每个用户的请求,并拒绝超出额度的请求(同时适用于 WebUI 和 API 请求)。
  6. 请求日志记录:记录所有传入的请求以进行分析、调试或计费。
  7. 语言检测:检测用户的语言并注入翻译指令,或路由至特定语言的模型。
  8. 提示词注入检测:扫描用户输入以查找试图操纵系统提示词的行为,并拦截恶意请求。
  9. 成本估算:在发送给模型之前估算输入 tokens,以进行预算追踪。
  10. A/B 测试:根据用户 ID 或随机选择,将用户路由至不同的模型配置。
💡 示例用例:以准备食材为例
🥗 示例 1:添加系统上下文

假设 LLM 是一位正在准备意大利美食的厨师,但用户尚未提及“这是为了做意大利菜”。您可以通过在将数据发往模型前追加此上下文,来确保方向是明确的。

async def inlet(self, body: dict, __user__: Optional[dict] = None) -> dict:
    # 在对话开始时为意大利菜背景添加系统消息
    context_message = {
        "role": "system",
        "content": "您正在帮助用户准备一顿意大利大餐。"
    }
    # 在聊天记录的开头插入该上下文
    body.setdefault("messages", []).insert(0, context_message)
    return body

📖 发生了什么?

  • 任何类似“有什么好的晚餐点子吗?”的用户输入,现在由于我们设置了系统上下文,都会带有意大利菜的主题!答案中可能不会出现芝士蛋糕,但一定会推荐意大利面。
🔪 示例 2:清洗输入(移除冗余字符)

假设用户输入看起来有些混乱,或包含了像 !!! 这样多余的符号,从而降低了对话效率或让模型难以解析。您可以在保留核心内容的同时对其进行清洗。

async def inlet(self, body: dict, __user__: Optional[dict] = None) -> dict:
    # 清理最后一条用户输入(来自 'messages' 列表的末尾)
    last_message = body["messages"][-1]["content"]
    body["messages"][-1]["content"] = last_message.replace("!!!", "").strip()
    return body

📖 发生了什么?

  • 转换前:"我该怎么调试这个问题!!!" ➡️ 发送给模型时:"我该怎么调试这个问题"
注意

用户的感受是一样的,但模型处理的是一个更加整洁且易于理解的查询。

📊 inlet 如何帮助优化 LLM 的输入:
  • 通过澄清模糊的查询来提升准确性
  • 通过移除不必要的杂音(如表情符号、HTML 标签或多余的标点符号)来使 AI 更加高效
  • 通过格式化用户输入以匹配模型预期的模式或 Schema(例如特定用例下的 JSON),从而确保一致性

💭 inlet 想象为您厨房里的副厨师长 — 确保进入模型(您的 AI “菜谱”)的每一项原料都经过了清洗、打理和完美的调味。输入越好,输出就越棒!


🆕 3️⃣ stream 钩子(Open WebUI 0.5.17 新增)

🔄 什么是 stream 钩子?

stream 函数是 Open WebUI 0.5.17 中引入的一项新功能,允许您实时拦截并修改流式的模型响应

与处理整条已完成响应的 outlet 不同,stream 在收到模型发送的**每一个数据块(chunk)**时立即对其进行处理。

🛠️ 何时使用 stream 钩子?
  • 实时内容过滤 - 在流式输出期间过滤脏话或敏感内容
  • 实时词语替换 - 实时替换品牌名称、竞品提及或过时的术语
  • 流式分析 - 实时计算 tokens 并追踪响应长度
  • 进度指示器 - 检测特定模式以展示加载状态
  • 调试 - 记录每个数据块以便排查流式传输问题
  • 格式纠正 - 纠正实时输出中出现的常见格式问题
📜 示例:记录流式块

以下是您如何检查和修改流式 LLM 响应的方式:

async def stream(self, event: dict) -> dict:
    print(event)  # 打印每一个传入的数据块以便检查
    return event

流式事件示例:

{"id": "chatcmpl-B4l99MMaP3QLGU5uV7BaBM0eDS0jb","choices": [{"delta": {"content": "Hi"}}]}
{"id": "chatcmpl-B4l99MMaP3QLGU5uV7BaBM0eDS0jb","choices": [{"delta": {"content": "!"}}]}
{"id": "chatcmpl-B4l99MMaP3QLGU5uV7BaBM0eDS0jb","choices": [{"delta": {"content": " 😊"}}]}

📖 发生了什么?

  • 每一行代表模型流式响应中的一个微小片段
  • delta.content 字段包含逐步生成的文本。
🔄 示例:从流式数据中过滤表情符号
async def stream(self, event: dict) -> dict:
    for choice in event.get("choices", []):
        delta = choice.get("delta", {})
        if "content" in delta:
            delta["content"] = delta["content"].replace("😊", "")  # 剥离表情符号
    return event

📖 转换前: "Hi 😊" 📖 转换后: "Hi"


4️⃣ outlet 函数(输出后置处理)

outlet 函数就像是一位校对员:在 LLM 完成处理之后,润色 AI 的响应(或进行最终的修改)。

📤 输入

  • body:这包含聊天中的所有当前消息(用户历史 + LLM 回复)。

🚀 您的任务:修改此 body。您可以清洗、追加或记录更改,但要留意每一次调整将如何影响用户体验。

💡 最佳实践

  • 在 outlet 内部,相较于直接编辑内容,应更倾向于进行日志记录(例如为了调试或分析)。
  • 如果需要进行繁重的修改(如大范围格式化输出),请考虑改用 Pipe 函数
🛠️ outlet 的使用场景:
  • 响应日志记录 - 追踪所有模型输出以便进行分析或合规审查
  • Token 使用量追踪 - 在完成后计算输出 tokens 以便计费
  • Langfuse/可观测性集成 - 将追踪指标发送给监控平台
  • 引用格式化 - 在最终输出中重新格式化参考链接
  • 免责声明注入 - 追加法律声明或 AI 生成披露声明
  • 响应缓存 - 存储响应供未来检索
  • 质量评分 - 针对模型输出运行自动化的质量检查
关于 Outlet 和 API 请求的说明

outlet() 无法在直接调用 /api/chat/completions 时可靠运行。在已发布的稳定版本上,该端点绝不会调用它。在 dev 上它可以内联运行,但仅在调用者提供 chat_id + id、拥有该聊天且使用的是非流式请求时才起作用 — 并且即使这样,过滤后的内容也不会在 HTTP 响应中返回。对于需要 outlet() 的直接 API 集成,请在 /api/chat/completions 之后紧接着请求 POST /api/chat/completed。详情请参阅 API 请求中的 Filter 行为

💡 示例用例:剥离您不希望用户看到的敏感 API 响应:

async def outlet(self, body: dict, __user__: Optional[dict] = None) -> dict:
    for message in body["messages"]:
        message["content"] = message["content"].replace("<API_KEY>", "[REDACTED]")
    return body

🌟 实战中的 Filters:构建实用示例

让我们构建一些真实的示例,看看您将如何使用 Filters!

📚 示例 #1:为每个用户输入添加上下文

希望 LLM 始终知道它正在帮助客户排查软件 bug 吗?您可以在每次用户查询中自动添加类似于 “您是一位软件排查助手” 的指令。

class Filter:
    async def inlet(self, body: dict, __user__: Optional[dict] = None) -> dict:
        context_message = {
            "role": "system",
            "content": "您是一位软件排查助手。"
        }
        body.setdefault("messages", []).insert(0, context_message)
        return body

📚 示例 #2:高亮输出内容以便阅读

要以 Markdown 或其他格式化风格返回输出吗?使用 outlet 函数!

class Filter:
    async def outlet(self, body: dict, __user__: Optional[dict] = None) -> dict:
        # 为每一条响应添加“高亮” Markdown 格式
        for message in body["messages"]:
            if message["role"] == "assistant":  # 针对模型的响应
                message["content"] = f"**{message['content']}**"  # 使用 Markdown 进行加粗高亮
        return body

🚧 易混淆概念:疑难解答 FAQ 🛑

Q: Filters 与 Pipe 函数有什么区别?

Filters 仅在发送给模型从模型返回的数据流经阶段对数据进行修改,通常不会与这些阶段之外的逻辑进行深度交互。相比之下,Pipes:

  • 可以集成外部 API,或显著改变后端的处理运作方式。
  • 将自定义逻辑直接作为一个全新的“模型”暴露出来。

Q: 我可以在 outlet 中进行繁重的后处理吗?

可以,但这不是最佳实践

  • Filters 的设计初衷是进行轻量级的修改或应用日志记录。
  • 如果需要进行繁重的数据加工与转换,请考虑改用 Pipe 函数

🎉 总结:为什么要构建 Filter 函数?

到现在,您已经学习了:

  1. Inlet 操纵用户输入(前置处理)。
  2. Stream 拦截并修改流式的模型输出(实时处理)。
  3. Outlet 微调 AI 的输出(后置处理)。
  4. Filters 最适合对数据流进行轻量级、实时的修改。
  5. 借助 Valves,您可以赋予用户动态调整 Filters 行为的能力,以实现量身定制的体验。

🚀 轮到您了:开始动手尝试吧!什么样的小微调或上下文补充能提升您的 Open WebUI 体验呢?构建 Filters 充满乐趣,使用起来又非常灵活,能将您的模型带上全新的高度!

祝您编码愉快!✨

This content is for informational purposes only and does not constitute a warranty, guarantee, or contractual commitment. Open WebUI is provided "as is." See your license for applicable terms.