如何将提示词与 GitHub 同步
LangSmith 提供了一个协作界面,用于创建、测试和迭代提示词。
虽然您可以在运行时将提示词从 LangSmith 动态获取到您的应用程序中,但您可能更倾向于将提示词与您自己的数据库或版本控制系统同步。为了支持此工作流,LangSmith 允许您通过 Webhook 接收提示词更新通知。
为什么要将提示词与 GitHub 同步?
- 版本控制:在熟悉的系统中,将您的提示词与应用程序代码一起进行版本控制。
- CI/CD 集成:当关键提示词发生变化时,触发自动化的暂存或生产部署。
先决条件
在我们开始之前,请确保您已设置以下内容
- GitHub 帐户:一个标准的 GitHub 帐户。
- GitHub 存储库:创建一个新的(或选择一个现有的)存储库,用于存储您的 LangSmith 提示词清单。这可以是与您的应用程序代码相同的存储库,也可以是专门用于提示词的存储库。
- GitHub 个人访问令牌 (PAT)
- LangSmith Webhook 不直接与 GitHub 交互——它们会调用您创建的中间服务器。
- 该服务器需要 GitHub PAT 才能进行身份验证并向您的存储库提交更改。
- 必须包含
repo
范围(public_repo
对于公共存储库已足够)。 - 前往 GitHub > Settings > Developer settings > Personal access tokens > Tokens (classic)。
- 点击 Generate new token (classic)。
- 为其命名(例如,“LangSmith 提示词同步”),设置过期时间,并选择所需的范围。
- 点击 Generate token 并立即复制——它将不会再次显示。
- 安全地存储令牌,并将其作为环境变量提供给您的服务器。
理解 LangSmith “提示词提交”和 Webhook
在 LangSmith 中,当您保存对提示词的更改时,您实际上是创建了一个新版本或一个“提示词提交”。这些提交可以触发 Webhook。
Webhook 将发送一个包含新提示词清单的 JSON 有效负载。
Webhook 有效负载示例
{
"prompt_id": "f33dcb51-eb17-47a5-83ca-64ac8a027a29",
"prompt_name": "My Prompt",
"commit_hash": "commit_hash_1234567890",
"created_at": "2021-01-01T00:00:00Z",
"created_by": "Jane Doe",
"manifest": {
"lc": 1,
"type": "constructor",
"id": ["langchain", "schema", "runnable", "RunnableSequence"],
"kwargs": {
"first": {
"lc": 1,
"type": "constructor",
"id": ["langchain", "prompts", "chat", "ChatPromptTemplate"],
"kwargs": {
"messages": [
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"chat",
"SystemMessagePromptTemplate"
],
"kwargs": {
"prompt": {
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"prompt",
"PromptTemplate"
],
"kwargs": {
"input_variables": [],
"template_format": "mustache",
"template": "You are a chatbot."
}
}
}
},
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"chat",
"HumanMessagePromptTemplate"
],
"kwargs": {
"prompt": {
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"prompt",
"PromptTemplate"
],
"kwargs": {
"input_variables": ["question"],
"template_format": "mustache",
"template": "{{question}}"
}
}
}
}
],
"input_variables": ["question"]
}
},
"last": {
"lc": 1,
"type": "constructor",
"id": ["langchain", "schema", "runnable", "RunnableBinding"],
"kwargs": {
"bound": {
"lc": 1,
"type": "constructor",
"id": ["langchain", "chat_models", "openai", "ChatOpenAI"],
"kwargs": {
"temperature": 1,
"top_p": 1,
"presence_penalty": 0,
"frequency_penalty": 0,
"model": "gpt-4.1-mini",
"extra_headers": {},
"openai_api_key": {
"id": ["OPENAI_API_KEY"],
"lc": 1,
"type": "secret"
}
}
},
"kwargs": {}
}
}
}
}
}
重要的是要理解,LangSmith 用于提示词提交的 Webhook 通常在工作区级别触发。这意味着如果您的 LangSmith 工作区中的任何提示词被修改并保存了“提示词提交”,Webhook 就会触发并发送该提示词的更新清单。有效负载可以通过提示词 ID 进行识别。您的接收服务器应该考虑到这一点进行设计。
实现用于 Webhook 接收的 FastAPI 服务器
为了在提示词更新时有效处理来自 LangSmith 的 Webhook 通知,需要一个中间服务器应用程序。该服务器将作为 LangSmith 发送的 HTTP POST 请求的接收器。在本指南中,我们将概述一个简单的 FastAPI 应用程序的创建,以实现此角色。
这个公共可访问的服务器将负责
- 接收 Webhook 请求:监听传入的 HTTP POST 请求。
- 解析有效负载:从请求正文中提取和解释 JSON 格式的提示词清单。
- 提交到 GitHub:以编程方式在您指定的 GitHub 存储库中创建一个新的提交,其中包含更新后的提示词清单。这确保了您的提示词保持版本控制,并与 LangSmith 中的更改同步。
对于部署,可以使用 Render.com(提供合适的免费套餐)、Vercel、Fly.io 或其他云提供商(AWS、GCP、Azure)等平台来托管 FastAPI 应用程序并获取公共 URL。
服务器的核心功能将包括一个用于 Webhook 接收的端点、用于解析清单的逻辑,以及与 GitHub API(使用个人访问令牌进行身份验证)的集成,以管理提交。
最小 FastAPI 服务器代码 (main.py
)
此服务器将监听来自 LangSmith 的传入 Webhook,并将收到的提示词清单提交到您的 GitHub 存储库。
import base64
import json
import uuid
from typing import Any, Dict
import httpx
from fastapi import FastAPI, HTTPException, Body
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
# --- Configuration ---
class AppConfig(BaseSettings):
"""
Application configuration model.
Loads settings from environment variables.
"""
GITHUB_TOKEN: str
GITHUB_REPO_OWNER: str
GITHUB_REPO_NAME: str
GITHUB_FILE_PATH: str = "prompt_manifest.json"
GITHUB_BRANCH: str = "main"
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding='utf-8',
extra='ignore'
)
settings = AppConfig()
# --- Pydantic Models ---
class WebhookPayload(BaseModel):
"""
Defines the expected structure of the incoming webhook payload.
"""
prompt_id: UUID = Field(
...,
description="The unique identifier for the prompt."
)
prompt_name: str = Field(
...,
description="The name/title of the prompt."
)
commit_hash: str = Field(
...,
description="An identifier for the commit event that triggered the webhook."
)
created_at: str = Field(
...,
description="Timestamp indicating when the event was created (ISO format preferred)."
)
created_by: str = Field(
...,
description="The name of the user who created the event."
)
manifest: Dict[str, Any] = Field(
...,
description="The main content or configuration data to be committed to GitHub."
)
# --- GitHub Helper Function ---
async def commit_manifest_to_github(payload: WebhookPayload) -> Dict[str, Any]:
"""
Helper function to commit the manifest directly to the configured branch.
"""
github_api_base_url = "https://api.github.com"
repo_file_url = (
f"{github_api_base_url}/repos/{settings.GITHUB_REPO_OWNER}/"
f"{settings.GITHUB_REPO_NAME}/contents/{settings.GITHUB_FILE_PATH}"
)
headers = {
"Authorization": f"Bearer {settings.GITHUB_TOKEN}",
"Accept": "application/vnd.github.v3+json",
"X-GitHub-Api-Version": "2022-11-28",
}
manifest_json_string = json.dumps(payload.manifest, indent=2)
content_base64 = base64.b64encode(manifest_json_string.encode('utf-8')).decode('utf-8')
commit_message = f"feat: Update {settings.GITHUB_FILE_PATH} via webhook - commit {payload.commit_hash}"
data_to_commit = {
"message": commit_message,
"content": content_base64,
"branch": settings.GITHUB_BRANCH,
}
async with httpx.AsyncClient() as client:
current_file_sha = None
try:
params_get = {"ref": settings.GITHUB_BRANCH}
response_get = await client.get(repo_file_url, headers=headers, params=params_get)
if response_get.status_code == 200:
current_file_sha = response_get.json().get("sha")
elif response_get.status_code != 404: # If not 404 (not found), it's an unexpected error
response_get.raise_for_status()
except httpx.HTTPStatusError as e:
error_detail = f"GitHub API error (GET file SHA): {e.response.status_code} - {e.response.text}"
print(f"[ERROR] {error_detail}")
raise HTTPException(status_code=e.response.status_code, detail=error_detail)
except httpx.RequestError as e:
error_detail = f"Network error connecting to GitHub (GET file SHA): {str(e)}"
print(f"[ERROR] {error_detail}")
raise HTTPException(status_code=503, detail=error_detail)
if current_file_sha:
data_to_commit["sha"] = current_file_sha
try:
response_put = await client.put(repo_file_url, headers=headers, json=data_to_commit)
response_put.raise_for_status()
return response_put.json()
except httpx.HTTPStatusError as e:
error_detail = f"GitHub API error (PUT content): {e.response.status_code} - {e.response.text}"
if e.response.status_code == 409: # Conflict
error_detail = (
f"GitHub API conflict (PUT content): {e.response.text}. "
"This might be due to an outdated SHA or branch protection rules."
)
elif e.response.status_code == 422: # Unprocessable Entity
error_detail = (
f"GitHub API Unprocessable Entity (PUT content): {e.response.text}. "
f"Ensure the branch '{settings.GITHUB_BRANCH}' exists and the payload is correctly formatted."
)
print(f"[ERROR] {error_detail}")
raise HTTPException(status_code=e.response.status_code, detail=error_detail)
except httpx.RequestError as e:
error_detail = f"Network error connecting to GitHub (PUT content): {str(e)}"
print(f"[ERROR] {error_detail}")
raise HTTPException(status_code=503, detail=error_detail)
# --- FastAPI Application ---
app = FastAPI(
title="Minimal Webhook to GitHub Commit Service",
description="Receives a webhook and commits its 'manifest' part directly to a GitHub repository.",
version="0.1.0",
)
@app.post("/webhook/commit", status_code=201, tags=["GitHub Webhooks"])
async def handle_webhook_direct_commit(payload: WebhookPayload = Body(...)):
"""
Webhook endpoint to receive events and commit DIRECTLY to the configured branch.
"""
try:
github_response = await commit_manifest_to_github(payload)
return {
"message": "Webhook received and manifest committed directly to GitHub successfully.",
"github_commit_details": github_response.get("commit", {}),
"github_content_details": github_response.get("content", {})
}
except HTTPException:
raise # Re-raise if it's an HTTPException from the helper
except Exception as e:
error_message = f"An unexpected error occurred: {str(e)}"
print(f"[ERROR] {error_message}")
raise HTTPException(status_code=500, detail="An internal server error occurred.")
@app.get("/health", status_code=200, tags=["Health"])
async def health_check():
"""
A simple health check endpoint.
"""
return {"status": "ok", "message": "Service is running."}
# To run this server (save as main.py):
# 1. Install dependencies: pip install fastapi uvicorn pydantic pydantic-settings httpx python-dotenv
# 2. Create a .env file with your GitHub token and repo details.
# 3. Run with Uvicorn: uvicorn main:app --reload
# 4. Deploy to a public platform like Render.com.
此服务器的关键方面
- 配置 (
.env
):它需要一个.env
文件,其中包含您的GITHUB_TOKEN
、GITHUB_REPO_OWNER
和GITHUB_REPO_NAME
。您还可以自定义GITHUB_FILE_PATH
(默认:LangSmith_prompt_manifest.json
)和GITHUB_BRANCH
(默认:main
)。 - GitHub 交互:
commit_manifest_to_github
函数处理获取当前文件 SHA(以进行更新)然后提交新清单内容的逻辑。 - Webhook 端点 (
/webhook/commit
):这是您的 LangSmith Webhook 将指向的 URL 路径。 - 错误处理:包含 GitHub API 交互的基本错误处理。
将此服务器部署到您选择的平台(例如 Render),并记下其公共 URL(例如 https://prompt-commit-webhook.onrender.com
)。
在 LangSmith 中配置 Webhook
一旦您的 FastAPI 服务器部署完毕并获得了其公共 URL,您就可以在 LangSmith 中配置 Webhook 了
-
导航到您的 LangSmith 工作区。
-
前往提示词部分。您将在此处看到您的提示词列表。
-
在“提示词”页面的右上角,点击+ Webhook按钮。
-
您将看到一个配置 Webhook 的表单
- Webhook URL:输入您已部署的 FastAPI 服务器端点的完整公共 URL。对于我们的示例服务器,这将是
https://prompt-commit-webhook.onrender.com/webhook/commit
。 - 标头(可选)
- 您可以添加 LangSmith 将随每个 Webhook 请求发送的自定义标头。
- Webhook URL:输入您已部署的 FastAPI 服务器端点的完整公共 URL。对于我们的示例服务器,这将是
-
测试 Webhook:LangSmith 提供了一个“发送测试通知”按钮。使用此按钮向您的服务器发送一个示例有效负载。检查您的服务器日志(例如,在 Render 上),以确保它接收请求并成功处理(或调试任何问题)。
-
保存 Webhook 配置。
工作流实战
现在,一切都设置好了,下面是发生的情况
-
提示词修改:用户(开发人员或非技术团队成员)在 LangSmith UI 中修改提示词并保存,从而创建一个新的“提示词提交”。
-
Webhook 触发:LangSmith 检测到此新的提示词提交并触发已配置的 Webhook。
-
HTTP 请求:LangSmith 向您的 FastAPI 服务器的公共 URL(例如
https://prompt-commit-webhook.onrender.com/webhook/commit
)发送一个 HTTP POST 请求。此请求的主体包含整个工作区的 JSON 提示词清单。 -
服务器接收有效负载:您的 FastAPI 服务器的端点接收请求。
-
GitHub 提交:服务器从请求正文中解析 JSON 清单。然后它使用配置的 GitHub 个人访问令牌、存储库所有者、存储库名称、文件路径和分支来
- 检查清单文件是否已存在于指定分支的存储库中以获取其 SHA(这对于更新现有文件是必需的)。
- 使用最新的提示词清单创建一个新的提交,无论是创建文件还是在文件已存在时更新它。提交消息将指示它是来自 LangSmith 的更新。
-
确认:您应该会在您的 GitHub 存储库中看到新的提交出现。
您现在已成功将您的 LangSmith 提示词与 GitHub 同步!
超越简单的提交
我们的示例 FastAPI 服务器直接提交整个提示词清单。然而,这仅仅是开始。您可以扩展服务器的功能以执行更复杂的动作
- 精细提交:如果您喜欢存储库中更精细的结构,可以解析清单并将更改提交到单个提示词文件。
- 触发 CI/CD:除了提交之外(或作为补充),让服务器触发 CI/CD 流水线(例如 Jenkins、GitHub Actions、GitLab CI)以部署暂存环境、运行测试或构建新的应用程序版本。
- 更新数据库/缓存:如果您的应用程序从数据库或缓存加载提示词,则直接更新这些存储。
- 通知:向 Slack、电子邮件或其他通信渠道发送有关提示词更改的通知。
- 选择性处理:根据 LangSmith 有效负载中的元数据(如果可用,例如哪个特定提示词更改了或由谁更改),您可以应用不同的逻辑。