主题
如何用 GitHub 同步提示
LangSmith 提供了一个协作式界面来创建、测试和迭代提示。
虽然你可以在运行时动态拉取提示到 LangSmith 中的应用,但你可能更希望将提示同步到你自己的数据库或版本控制系统。为支持这一工作流,LangSmith 允许你通过 webhook 接收提示更新的提醒。
为什么要用 GitHub 同步提示?
- 版本控制:在你的应用中,让提示在一个熟悉的系统里与你的应用代码一同进行版本管理。
- CI/CD 集成:当关键提示变更时触发自动的 staging 或生产部署。

前置条件
在开始之前,确保你已设置好以下内容:
GitHub 账户: 一个标准的 GitHub 账户。
GitHub 仓库: 创建一个新仓库(或选择一个现有的)用于存储你的 LangSmith 提示清单。这可以与你的应用代码同一个仓库,或专门为提示建一个。
GitHub 个人访问令牌(PAT):
- LangSmith webhooks 不直接与 GitHub 交互——它们调用一个由 你 创建的中间服务器。
- 该服务器需要一个 GitHub PAT 来进行身份验证并向你的仓库提交。
- 必须包含
reposcope(对于公共仓库public_repo即可)。 - 前往 GitHub > Settings > Developer settings > Personal access tokens > Tokens (classic)。
- 点击 Generate new token (classic)。
- 为其命名(例如 "LangSmith Prompt Sync"),设置一个过期时间,并选择所需的 scopes。
- 点击 Generate token 并 立即复制它,因为它不会再显示一次。
- 安全地存储该令牌,并将其作为环境变量提供给服务器。
理解 LangSmith 的"提示提交"和 webhook
在 LangSmith 中,当你对提示保存更改时,你实际上是在创建一个新版本或一次"提示提交(Prompt Commit)"。正是这些提交可以触发 webhook。
该 webhook 会发送一个包含新提示清单的 JSON 负载。
示例 Webhook 负载
json
{
"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-5.4-mini",
"extra_headers": {},
"openai_api_key": {
"id": ["OPENAI_API_KEY"],
"lc": 1,
"type": "secret"
}
}
},
"kwargs": {}
}
}
}
}
}NOTE
需要理解的重要一点是,用于提示提交的 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 的集成(使用个人访问令牌进行身份验证)来管理提交。
Minimal FastAPI Server Code ()
main.py
该服务器会监听来自 LangSmith 的传入 webhook,并将接收到的提示提交到你的 GitHub 仓库。
python
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): 它期望一个包含你的GITHUB_TOKEN、GITHUB_REPO_OWNER和GITHUB_REPO_NAME的.env文件。你也可以自定义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://<your-render-service>.onrender.com)。
在 LangSmith 中配置 webhook
一旦你的 FastAPI 服务器被部署并且你有了它的公共 URL,你就可以在 LangSmith 中配置该 webhook:
导航到你的 LangSmith 工作区。
前往 Prompts 部分。在此你会看到你的提示列表。

在 Prompts 页面的右上角,点击 + Webhook 按钮。
你会看到一个用于配置 webhook 的表单:

- Webhook URL: 输入你已部署的 FastAPI 服务器端点的完整公共 URL。对于我们示例服务器,这将是
https://<your-render-service>.onrender.com/webhook/commit。 - 标头(可选):
- 你可以添加 LangSmith 将随每个 webhook 请求发送的自定义标头。
- Webhook URL: 输入你已部署的 FastAPI 服务器端点的完整公共 URL。对于我们示例服务器,这将是
测试 webhook: LangSmith 提供了一个 "Send Test Notification" 按钮。使用它向你的服务器发送一个示例负载。检查你的服务器日志(例如在 Render 上),以确保它成功接收请求并处理它(或调试任何问题)。
保存 webhook 配置。
实际运行的工作流

现在,一切设置就绪,接下来会发生:
提示修改: 一个用户(开发者或非技术团队成员)在 LangSmith UI 中修改一个提示并保存它,创建一次新的"提示提交"。
Webhook 触发: LangSmith 检测到这次新的提示提交并触发配置好的 webhook。
HTTP 请求: LangSmith 向你的 FastAPI 服务器的公共 URL(例如
https://<your-render-service>.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)来部署 staging 环境、运行测试或构建新的应用版本,以此替代提交(或在其之外)。
- 更新数据库/缓存: 如果你的应用从数据库或缓存加载提示,直接更新这些存储。
- 提醒: 就提示更改向 Slack、邮件或其他通信渠道发送提醒。
- 选择性处理: 基于 LangSmith 负载内的元数据(如果可用,例如具体哪个提示被谁更改),你可以应用不同的逻辑。