A lightweight, semantic ad-engine for the LLMs, available through MCP
🐦 Follow Updates • 📧 Contact & Feedback
一个向LLM提供广告的简单MCP服务器!使用此功能在法学硕士课程中注入赞助商的广告。
介绍
AdKit 是一个轻量级的语义广告匹配引擎,专为 LLM应用。它通过以下方式暴露出一个小而安全的工具表面 主控程序 因此,代理可以使用自然语言上下文(聊天轮次、页面内容、搜索查询)请求相关广告,而无需脆弱的关键字规则。
在幕后,AdKit在本地嵌入您的上下文(FastEmbed),并从以下位置检索候选人 Qdrant 使用向量相似度加 类型化约束 (主题、区域设置、垂直领域、排除项、策略标志)。它的设计有一个硬安全边界: 数据平面 是只读和分配的,而 控制平面 分别处理摄取和管理操作。
当您希望“原生”赞助商插入或产品推荐与含义相匹配,而不是字符串时,请使用它——并且您希望架构在交付生产时保持理智。
你在哪里可以使用这个?
- 人工智能代理和助理:无缝地将相关的产品推荐或赞助信息注入聊天界面(例如,客户支持机器人、购物助理)。
- RAG(检索增强代)管道:在有机检索结果旁边提供“赞助上下文”,允许在搜索或问答工具中提供高相关性的原生广告。
- 内容发现平台:基于所消费内容的语义含义,而不是脆弱的关键字匹配,增强“您可能也喜欢”功能或联盟链接插入。
或 从建筑中汲取灵感
先决条件
设置
安装uv
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# Or with pip
pip install uv在本地启动Qdrant
# Using Docker
docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant
# Or download binary from https://github.com/qdrant/qdrant/releases安装依赖项
# Install all dependencies and create virtual environment
uv sync配置环境(可选)
# Copy example env file (defaults work for local Qdrant)
cp .env.example .env演示广告设置(分步)
按照以下步骤加载演示广告并确认一切正常。演示广告定义见 data/test_ads.json;重新运行 seed 打乱它们,使商店与文件匹配。
步骤1。 启动Qdrant(在终端中):
docker run -d --name qdrant \
-p 6333:6333 -p 6334:6334 \
qdrant/qdrant
docker ps --filter name=qdrant步骤2。 在项目目录中,安装依赖项:
uv sync步骤3。 (可选)复制环境:
cp .env.example .env步骤4。 创建集合:
uv run ad-index create 如果存在(uv运行广告索引删除)
步骤5。 从文件加载演示广告:
uv run ad-index seed要使用其他文件,请执行以下操作: uv run ad-index seed --file path/to/ads.json
步骤6。 验证它是否有效:
- 运行:
uv run ad-index info确认 积分计数 是5(或JSON文件中的广告数量)。
- 可选地,从Python查询广告以确认它们正在被提供:
uv run python -c "
from ad_injector.wiring import build_match_service
from ad_injector.models.mcp_requests import MatchRequest
r, _ = build_match_service().match(MatchRequest(context_text='python', top_k=2))
print(r.model_dump_json(indent=2))
"您应该在输出中看到匹配的广告(例如Python/编码广告)。
建筑
该系统分为两个MCP服务器平面:
| 飞机 | 目的 | 谁叫它 | 入口点 |
|---|---|---|---|
| 数据平面 | 广告匹配、只读检索 | LLM/代理 | uv run ad-mcp-data 或 uv run ad-data-plane |
| 控制平面 | 配置、接收、管理操作 | 人员、CI/CD、后台 | uv run ad-mcp-control 或 uv run ad-index (CLI) |
为生产运行两个单独的进程:一个控制平面(管理)和一个数据平面(运行时)。每个都有自己的身份验证范围(可选 MCP_ADMIN_KEY / MCP_DATA_KEY).
数据平面工具(运行时,面向LLM)
ads_match--语义广告匹配(上下文文本、位置、约束、top_k);返回候选项和match_id进行解释ads_explain--先前匹配的审计跟踪(match_id)ads_health--活性/准备就绪(Qdrant+嵌入)ads_capabilities--支持的布局、约束键、嵌入模型、模式版本
数据平面使用显式的allowlist(DATA_PLANE_ALLOWED_TOOLS).无法注册任何破坏性或管理工具。
控制平面工具(管理员)
collection_ensure--创建/对齐集合(维度、嵌入模型id、模式版本)collection_info--集合元数据(点数、维度、嵌入模型id、模式版本)collection_migrate--可选的模式迁移(从版本到版本)ads_upsert_batch--批量广告摄取(JSON数组)ads_delete--按id删除广告ads_bulk_disable--set enabled=false,用于匹配过滤器(JSON过滤器)的广告ads_get--获取单个广告(调试)
回购结构
src/ad_injector/
models/ # Ad, Targeting, Policy; MCP request/response DTOs
services/ # MatchService, PolicyEngine, TargetingEngine, IndexService
adapters/ # QdrantVectorStore, FastEmbedProvider
mcp/ # server, tools, auth, observability
config/ # RuntimeSettings, env vars
ops/ # smoke_check, migrations配置
运行时设置通过环境变量(或 .env),在启动时由Pydantic验证:
| 变量 | 默认值 | 描述 |
|---|---|---|
QDRANT_HOST | localhost | Qdrant服务器主机 |
QDRANT_PORT | 6333 | Qdrant服务器端口 |
QDRANT_COLLECTION_NAME | ads | 收藏名称 |
EMBEDDING_MODEL_ID | BAAI/bge-small-en-v1.5 | 嵌入模型 |
EMBEDDING_DIMENSION | 384 | 矢量维度 |
MAX_TOP_K | 100 | 每次匹配查询的最大结果数 |
MAX_BATCH_SIZE | 500 | 每个追加销售批次的最大广告数 |
REQUEST_TIMEOUT_SECONDS | 30.0 | 每次请求超时 |
REQUIRE_ADMIN_KEY | false | 如果为真,控制平面需要 MCP_ADMIN_KEY env |
REQUIRE_DATA_KEY | false | 如果为真,数据平面需要 MCP_DATA_KEY env |
用紫外线跑步
运行脚本
# Data Plane MCP server (LLM-facing, read-only): ads_match, ads_explain, ads_health, ads_capabilities
uv run ad-mcp-data
# or: uv run ad-data-plane
# Control Plane MCP server (admin): collection.*, ads.upsert_batch, ads.delete, ads.bulk_disable, ads.get
uv run ad-mcp-control
# CLI (Control Plane): create collection, seed ads, info, delete
uv run ad-index create # Create the collection
uv run ad-index seed # Add sample ads for testing
uv run ad-index info # Show collection info
uv run ad-index delete # Delete the collection直接运行Python文件
uv run python -m ad_injector.main_runtime # Data Plane MCP
uv run python -m ad_injector.main_control # Control Plane MCP
uv run python -m ad_injector.cli create # Control Plane CLI
uv run python -m ad_injector.cli seed备注:The seed 命令从加载演示广告 data/test_ads.json (或 --file )并将它们加入收藏。跑 create 首先设置集合,然后 seed 加载测试数据。
正在验证MCP服务器
1.运行测试套件
uv run pytest tests/ -v这将运行数据平面护栏测试,该测试断言:
- 数据平面仅公开已分配的工具(
ads_match,ads_explain,ads_health,ads_capabilities) - 数据平面上没有禁用/破坏性工具
- Control Plane有管理工具 不 暴露数据平面–仅限工具
2.验证数据平面是否公开了分配的工具
uv run python -c "
from ad_injector.mcp.server import create_server
from ad_injector.mcp.tools import DATA_PLANE_ALLOWED_TOOLS
s = create_server('data')
tools = set(s._tool_manager._tools.keys())
print(f'Server: {s.name}')
print(f'Tools: {tools}')
assert tools == DATA_PLANE_ALLOWED_TOOLS, f'FAIL: expected {DATA_PLANE_ALLOWED_TOOLS}'
print('PASS: Data Plane allowlist registered')
"3.使用管理工具验证控制平面是否启动
uv run python -c "
from ad_injector.mcp.server import create_server
s = create_server('admin')
tools = set(s._tool_manager._tools.keys())
print(f'Server: {s.name}')
print(f'Tools: {tools}')
assert 'ads_match' not in tools, 'FAIL: ads_match on admin plane'
assert 'collection_ensure' in tools
print('PASS: admin tools registered, no ads_match')
"4.验证ads_match DTO验证
uv run python -c "
from ad_injector.models import MatchRequest, MatchConstraints, PlacementContext, MatchResponse, AdCandidate
# Valid request
req = MatchRequest(
context_text='I want to learn Python',
top_k=5,
placement=PlacementContext(placement='sidebar', surface='chat'),
constraints=MatchConstraints(topics=['python'], locale='en-US', sensitive_ok=False),
)
print(f'MatchRequest OK: context_text={req.context_text!r}, top_k={req.top_k}')
print(f' constraints.topics={req.constraints.topics}, locale={req.constraints.locale}')
# Valid response
resp = MatchResponse(
candidates=[AdCandidate(ad_id='ad-001', advertiser_id='adv-1', title='Learn Python',
body='Courses', cta_text='Go', landing_url='https://example.com', score=0.95, match_id='m-1')],
request_id='req-xyz', placement='sidebar',
)
print(f'MatchResponse OK: {len(resp.candidates)} candidate(s)')
# Invalid request (empty context) fails
try:
MatchRequest(context_text='', top_k=5)
print('FAIL: empty context_text should be rejected')
except Exception:
print('PASS: empty context_text rejected')
"5.验证配置加载和验证
# Defaults
uv run python -c "
from ad_injector.config import get_settings
s = get_settings()
print(f'host={s.qdrant_host} port={s.qdrant_port} model={s.embedding_model_id}')
"
# Invalid port fails fast
QDRANT_PORT=99999 uv run python -c "from ad_injector.config.runtime import RuntimeSettings; RuntimeSettings()" 2>&1 | head -36.验证导入隔离(数据平面不加载管理代码)
uv run python -c "
import sys
from ad_injector.main_runtime import main
mods = [m for m in sys.modules if m.startswith('ad_injector')]
assert 'ad_injector.cli' not in mods, 'FAIL: cli imported'
print('PASS: main_runtime has clean import graph (no admin modules)')
"添加依赖关系
uv add
# Add a dependency
uv add --dev
# Add a dev dependency广告模式
Qdrant中存储的每个广告都包含:
| 字段 | 类型 | 描述 |
|---|---|---|
ad_id | string | 广告的唯一标识符 |
advertiser_id | string | 广告商的标识符 |
title | string | 广告标题 |
body | string | 广告正文 |
cta_text | string | 行动呼吁文本 |
landing_url | string | 重定向URL |
targeting.topics | string\[\] | 要定位的主题 |
targeting.locale | 字符串\[\] | 本地代码(例如,“en-us”) |
targeting.verticals | string\[\] | 垂直行业 |
targeting.blocked_keywords | string\[\] | 要排除的关键字 |
policy.sensitive | boolean | 敏感内容标志 |
policy.age_restricted | boolean | 年龄限制标志 |
enabled | boolean | 广告是否符合匹配条件(默认 true; ads_bulk_disable 套 false) |
嵌入文本:向量嵌入是从以下内容生成的 title + body + topics.
用法示例
from ad_injector.models import Ad, AdTargeting, AdPolicy
from ad_injector.wiring import build_index_service, build_match_service
from ad_injector.models.mcp_requests import MatchRequest
# Create the collection (once) and seed ads via IndexService
index_svc = build_index_service()
index_svc.ensure_collection()
ad = Ad(
ad_id="ad-001",
advertiser_id="adv-123",
title="Learn Python Today",
body="Master Python programming with our interactive courses.",
cta_text="Start Learning",
landing_url="https://example.com/python",
targeting=AdTargeting(
topics=["programming", "python", "education"],
locale=["en-US"],
verticals=["education", "technology"],
),
policy=AdPolicy(sensitive=False, age_restricted=False),
)
index_svc.upsert_ads([ad])
# Match ads via MatchService (Data Plane logic)
match_svc = build_match_service()
response, audit_trace = match_svc.match(
MatchRequest(context_text="python tutorial", top_k=5)
)
for c in response.candidates:
print(f"{c.ad_id}: {c.title} (score={c.score}, match_id={c.match_id})")ads_match 请求/响应模式
数据平面 ads_match 该工具使用类型化的DTO——不接受原始字典过滤器。
请求参数
| 参数 | 类型 | 默认值 | 说明 | |
|---|---|---|---|---|
context_text | 字符串(1-10000个字符) | *必需的* | 要匹配的对话/页面上下文 | |
top_k | 整数(1-100) | 5 | 返回的候选人数量 | |
placement | 字符串 | "inline" | 放置槽(例如。 inline, sidebar, banner) | |
surface | 字符串 | "chat" | 表面类型(例如。 chat, search, feed) | |
topics | string\[\] | 空 | null | 仅限于这些主题 |
locale | string | 空 | null | 所需的区域设置(例如。 en-US) |
verticals | string\[\] | 空 | null | 仅限于这些垂直领域 |
exclude_advertiser_ids | string\[\] | 空 | null | 要排除的广告商ID |
exclude_ad_ids | string\[\] | 空 | null | 要排除的广告ID |
age_restricted_ok bool的。 false | 允许年龄限制广告 | |||
sensitive_ok bool的。 false | 允许敏感内容广告 |
响应形状
{
"candidates": [
{
"ad_id": "ad-001",
"advertiser_id": "adv-123",
"title": "Learn Python Today",
"body": "Master Python programming...",
"cta_text": "Start Learning",
"landing_url": "https://example.com/python",
"score": 0.95,
"match_id": "m-abc123"
}
],
"request_id": "req-xyz-456",
"placement": "sidebar"
}match_id可以传递给ads_explain用于审计跟踪(为什么符合/不符合条件、过滤器、分数)score是余弦相似度(0-1)
跟我说
我总是热衷于研究MCP工具、检索系统和实用的LLM货币化。\ 如果你正在构建类似的东西,或者想对你的架构进行压力测试,请联系:
- 🐦 推特:https://twitter.com/charoori_ai
- 📧 电子邮件:mailto:chandrahas.aroori@gmail.com主题=AdKit
