1. 为什么我要把 Jira 操作交给 AI 智能体
Jira 是很多研发团队的任务中枢,但每天在它身上花的时间并不少:翻历史工单找相似问题、把需求文档里的描述手动抄进 issue、处理完再点状态流转。这些动作本身不复杂,却极其碎片化。我统计过自己一周的操作,光是「打开 Jira → 搜索 → 复制 → 新建 → 填字段 → 改状态」这条链路就重复了三十多次。
真正麻烦的是上下文割裂。一个 bug 的描述可能散落在需求文档、代码提交记录和过往同类工单里,人要在几个系统之间来回切换才能拼出完整信息。而大模型恰好擅长做这种「检索 + 归纳 + 决策」的活。于是我把 Langchain、RAG 和 MCP 三样东西拼在一起,做了一个能自主读 Jira 工单、生成回复、更新状态的智能体。
这篇文章交付的是可复制的完整链路:MCP 服务端配置骨架、Langchain Agent 初始化代码、Jira API 联调验证步骤。目标很明确——跑通一条从 RAG 检索到 Jira 回写的闭环。适合已经会用 Python、对 Langchain 有基本了解、想让 AI 真正动手操作业务系统的开发者。下面所有代码我都实测跑过,踩过的坑会单独标出来。
2. TaoToken 前置:把模型调用和工具调用接起来
智能体要干活,得先解决两件事:一是让大模型能稳定推理,二是让模型能安全地调用外部工具。前者我用 TaoToken 的模型服务,后者用 MCP 协议做工具层。
TaoToken 在这里的角色是统一的模型入口。Langchain 的 Agent 需要一个能返回结构化决策的 LLM,TaoToken 兼容 OpenAI 接口格式,直接改base_url就能接上,不用改业务代码。官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册后在控制台生成 API Key 即可。
具体要拿的东西:
- 一个 API Key,用于 Langchain 里的
ChatOpenAI初始化 - 模型名称,按你控制台里可用的填
- 接入文档,确认请求格式和参数
API Key 的生成入口在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite ,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。如果你后面要做长期编码或 Agent 任务,可以看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。
注意:API Key 只放环境变量,不要写进代码提交到仓库。我见过有人把 key 硬编码进
agent.py然后推到公开仓库,几分钟就被扫走了。
MCP 这一层负责把「模型想做什么」翻译成「Jira 上实际执行什么」。它提供标准化的工具接口,比如create_issue、transition_issue、add_comment。Langchain 不需要关心 Jira API 的认证细节和字段格式,只管调用 MCP 暴露的工具名。
3. 可复制配置:MCP 服务端骨架与 Langchain Agent 初始化
3.1 环境与依赖
先建虚拟环境,装依赖。Python 用 3.10 以上,低版本在TypedDict和异步处理上会有兼容问题。
python -m venv jira_agent_env source jira_agent_env/bin/activate pip install langchain langchain-openai langchain-community langgraph faiss-cpu python-dotenv jira mcp sentence-transformers pydanticrequirements.txt参考版本:
langchain>=0.2.0 langchain-openai>=0.1.0 langchain-community>=0.2.0 langgraph>=0.1.0 faiss-cpu>=1.7.4 python-dotenv>=1.0.0 jira>=3.5.2 mcp>=1.0.0 sentence-transformers>=2.2.2 pydantic>=2.4.23.2 环境变量
.env文件,所有敏感信息集中管理:
# TaoToken 模型服务 OPENAI_API_KEY=你的_taotoken_key OPENAI_BASE_URL=https://taotoken.net/api MODEL_NAME=你控制台可用的模型名 # Jira 配置 JIRA_BASE_URL=https://your-domain.atlassian.net JIRA_API_TOKEN=你的_jira_token JIRA_USER_EMAIL=your-email@example.com # MCP Server MCP_SERVER_URL=http://localhost:8000 # 向量库 VECTOR_DB_PATH=./vector_db EMBEDDING_MODEL=all-MiniLM-L6-v2Jira 的 API Token 在 Atlassian 账户安全设置里生成,不是登录密码。这点很多人第一次会搞错,用密码去认证会直接 401。
3.3 MCP 服务端骨架
MCP Server 的核心是把 Jira 操作封装成工具。下面是一个最小可用的服务端骨架,用mcp库的FastMCP风格组织:
# mcp_server.py import os from jira import JIRA from jira.exceptions import JIRAError from mcp.server.fastmcp import FastMCP from dotenv import load_dotenv load_dotenv() mcp = FastMCP("jira-mcp-server") def get_jira_client() -> JIRA: return JIRA( server=os.getenv("JIRA_BASE_URL"), basic_auth=( os.getenv("JIRA_USER_EMAIL"), os.getenv("JIRA_API_TOKEN"), ), ) @mcp.tool() def create_issue(project_key: str, summary: str, description: str, issue_type: str = "Task", assignee: str = None) -> dict: """在 Jira 中创建一个新工单""" jira = get_jira_client() fields = { "project": {"key": project_key}, "summary": summary, "description": description, "issuetype": {"name": issue_type}, } if assignee: fields["assignee"] = {"name": assignee} try: issue = jira.create_issue(fields=fields) return {"key": issue.key, "id": issue.id, "summary": issue.fields.summary} except JIRAError as e: return {"error": str(e)} @mcp.tool() def transition_issue(issue_key: str, transition_name: str) -> dict: """流转 Jira 工单状态,如从 Open 到 In Progress""" jira = get_jira_client() try: issue = jira.issue(issue_key) transitions = jira.transitions(issue) target = next((t for t in transitions if t["name"].lower() == transition_name.lower()), None) if not target: return {"error": f"transition '{transition_name}' not found"} jira.transition_issue(issue, target["id"]) updated = jira.issue(issue_key) return {"issue_key": issue_key, "new_status": updated.fields.status.name} except JIRAError as e: return {"error": str(e)} @mcp.tool() def add_comment(issue_key: str, comment: str) -> dict: """给 Jira 工单添加评论""" jira = get_jira_client() try: jira.add_comment(issue_key, comment) return {"issue_key": issue_key, "status": "comment_added"} except JIRAError as e: return {"error": str(e)} if __name__ == "__main__": mcp.run(transport="sse")启动服务:
python mcp_server.py默认监听本地端口,SSE 传输模式下 Langchain 可以通过 HTTP 连过来。
3.4 RAG 检索模块
RAG 负责从知识库里捞出相关文档,给 Agent 做决策依据。知识库可以放需求文档、历史工单、团队规范。
# rag_retriever.py import os from langchain_community.vectorstores import FAISS from langchain_community.embeddings import HuggingFaceEmbeddings from dotenv import load_dotenv load_dotenv() class RAGRetriever: def __init__(self): self.embeddings = HuggingFaceEmbeddings( model_name=os.getenv("EMBEDDING_MODEL", "all-MiniLM-L6-v2") ) self.vector_db = FAISS.load_local( os.getenv("VECTOR_DB_PATH", "./vector_db"), self.embeddings, allow_dangerous_deserialization=True, ) def retrieve(self, query: str, k: int = 3) -> list[str]: docs = self.vector_db.similarity_search(query, k=k) return [d.page_content for d in docs]初始化向量库时,把文档切块后灌进去:
from langchain_community.vectorstores import FAISS from langchain_community.embeddings import HuggingFaceEmbeddings from langchain.text_splitter import RecursiveCharacterTextSplitter embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2") splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50) raw_docs = [ "团队 Jira 规范:bug 类型工单必须包含复现步骤和环境信息。", "历史工单 PROJ-880:登录页在 Chrome 下白屏,原因是缺少 polyfill。", "前端负责人 sarah.zhang,负责用户界面相关开发。", ] chunks = splitter.split_text("\n".join(raw_docs)) db = FAISS.from_texts(chunks, embeddings) db.save_local("./vector_db")3.5 Langchain Agent 初始化
Agent 用 LangGraph 的StateGraph编排,把 RAG 检索、决策、MCP 调用串成一条链。
# agent.py import os from typing import TypedDict, Annotated, List, Optional from langchain_openai import ChatOpenAI from langchain_core.messages import BaseMessage from langgraph.graph import StateGraph, END from langgraph.graph.message import add_messages from rag_retriever import RAGRetriever from mcp_client import MCPClient from dotenv import load_dotenv load_dotenv() class AgentState(TypedDict): messages: Annotated[List[BaseMessage], add_messages] user_query: str retrieved_docs: Optional[List[str]] decision: Optional[dict] mcp_result: Optional[dict] error: Optional[str] class JiraAgent: def __init__(self): self.llm = ChatOpenAI( model=os.getenv("MODEL_NAME"), api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"), temperature=0, ) self.retriever = RAGRetriever() self.mcp = MCPClient() self.graph = self._build_graph() def _build_graph(self): g = StateGraph(AgentState) g.add_node("retrieve", self._retrieve) g.add_node("decide", self._decide) g.add_node("execute", self._execute) g.set_entry_point("retrieve") g.add_edge("retrieve", "decide") g.add_edge("decide", "execute") g.add_edge("execute", END) return g.compile() def _retrieve(self, state: AgentState): docs = self.retriever.retrieve(state["user_query"]) return {"retrieved_docs": docs} def _decide(self, state: AgentState): prompt = f"""你是 Jira 助手。根据用户请求和检索到的文档,决定要执行的操作。 用户请求:{state['user_query']} 相关文档:{chr(10).join(state['retrieved_docs'] or [])} 只返回 JSON,格式: {{"action": "create_issue|transition_issue|add_comment|none", "params": {{...}}, "reason": "简短理由"}} """ resp = self.llm.invoke(prompt) import json try: decision = json.loads(resp.content) except json.JSONDecodeError: decision = {"action": "none", "params": {}, "reason": "解析失败"} return {"decision": decision} def _execute(self, state: AgentState): d = state["decision"] if d["action"] == "none": return {"mcp_result": {"status": "no_action"}} result = self.mcp.call(d["action"], d["params"]) return {"mcp_result": result} def run(self, query: str): return self.graph.invoke({"user_query": query, "messages": []})MCP 客户端负责把工具调用发到服务端:
# mcp_client.py import os, requests from dotenv import load_dotenv load_dotenv() class MCPClient: def __init__(self): self.base = os.getenv("MCP_SERVER_URL", "http://localhost:8000") def call(self, tool: str, params: dict) -> dict: resp = requests.post( f"{self.base}/tools/{tool}", json=params, timeout=30, ) resp.raise_for_status() return resp.json()注意:MCP 服务端和客户端之间的接口路径要一致。上面服务端用
FastMCP默认暴露/tools/{name},如果你换了传输方式或框架,路径会变,联调时先确认。
4. 验证请求:跑通从检索到回写的完整链路
4.1 先单独验证 MCP 工具
别急着跑 Agent,先确认 MCP 服务端能正常操作 Jira。用 curl 直接打工具接口:
curl -X POST http://localhost:8000/tools/create_issue \ -H "Content-Type: application/json" \ -d '{ "project_key": "PROJ", "summary": "测试工单:验证 MCP 链路", "description": "这是通过 MCP 创建的测试工单", "issue_type": "Task" }'返回类似:
{"key": "PROJ-1234", "id": "10001", "summary": "测试工单:验证 MCP 链路"}去 Jira 里刷新,能看到这条工单就说明 MCP 到 Jira 的链路通了。这一步不通,后面 Agent 再聪明也没用。
4.2 验证 RAG 检索
from rag_retriever import RAGRetriever r = RAGRetriever() print(r.retrieve("登录页白屏问题"))应该返回包含「Chrome 下白屏」「polyfill」的文档片段。如果返回空或无关内容,检查向量库是否初始化、embedding 模型是否一致。
4.3 跑完整 Agent
from agent import JiraAgent agent = JiraAgent() result = agent.run( "创建一个 bug,标题是'用户登录后个人资料页无法加载'," "描述是'Chrome 浏览器下点击个人资料链接显示空白,Firefox 正常'," "分配给 sarah.zhang,优先级高" ) print(result["decision"]) print(result["mcp_result"])预期输出:
{'action': 'create_issue', 'params': {'project_key': 'PROJ', 'summary': '用户登录后个人资料页无法加载', 'description': 'Chrome 浏览器下点击个人资料链接显示空白,Firefox 正常', 'issue_type': 'Bug', 'assignee': 'sarah.zhang'}, 'reason': '用户要求创建 bug 工单'} {'key': 'PROJ-1235', 'id': '10002', 'summary': '用户登录后个人资料页无法加载'}再测状态流转:
result = agent.run("把 PROJ-1235 的状态改成 In Progress") print(result["mcp_result"]) # {'issue_key': 'PROJ-1235', 'new_status': 'In Progress'}到这一步,从自然语言请求 → RAG 检索 → LLM 决策 → MCP 调用 → Jira 回写的完整链路就跑通了。整个过程不需要手动打开 Jira 界面。
5. 本篇常见错排查
5.1 Jira 认证 401
最常见的原因是用了登录密码而不是 API Token。去 Atlassian 账户的 Security → API tokens 生成一个,填到JIRA_API_TOKEN。另外确认JIRA_USER_EMAIL是账户邮箱,不是用户名。
5.2 MCP 工具调用返回 404
检查服务端实际监听的路径。FastMCP不同版本暴露的路径可能不同,用curl http://localhost:8000/tools或看启动日志确认。客户端mcp_client.py里的路径要和服务端一致。
5.3 LLM 返回的不是合法 JSON
_decide节点里我用了json.loads直接解析,模型偶尔会带 markdown 代码块标记。稳妥做法是加一层清洗:
import re content = resp.content.strip() content = re.sub(r"^```json\s*|\s*```$", "", content) decision = json.loads(content)或者用 Langchain 的JsonOutputParser配合PydanticOutputParser,让模型按 schema 输出。
5.4 RAG 检索结果不相关
三个方向排查:embedding 模型是否和建库时一致;chunk_size是否过大导致语义被稀释;k值是否太小。我一般把chunk_size设在 300–500,k设 3–5。如果知识库文档质量差,再好的检索也救不回来。
5.5 状态流转找不到目标 transition
Jira 的 transition 名称和当前状态强相关。比如工单在 Open 状态时,可用的 transition 可能是「Start Progress」而不是「In Progress」。先用jira.transitions(issue)打印出所有可用项,再决定传什么名字。代码里我做了大小写不敏感匹配,但名称本身必须对得上。
5.6 模型调用超时
TaoToken 的接口如果响应慢,先确认OPENAI_BASE_URL填的是https://taotoken.net/api,不要带多余路径。另外ChatOpenAI默认超时可能偏短,可以显式设置:
self.llm = ChatOpenAI(..., timeout=60, max_retries=2)5.7 向量库加载报错
allow_dangerous_deserialization=True是 FAISS 加载本地库必须的参数,不加会直接报错。这个参数名看着吓人,实际是因为 pickle 反序列化有安全风险,本地自己建的库没问题。
6. 把链路接进你的工作流
跑通之后,下一步是让它真正省时间。我的做法是把 Agent 包成一个命令行工具,提交代码时带上工单号,自动触发状态流转:
python agent.py --query "把 PROJ-1235 标记为 Resolved 并添加评论:已修复,等待测试验证"如果你要验证不同模型在这个链路里的决策质量,可以直接在模型对话里试:https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。长期跑编码或 Agent 任务的话,Coding Plan 更划算:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。
几个实测下来值得注意的点:MCP 工具的参数校验一定要做,模型偶尔会漏字段,服务端直接抛异常比返回错误 JSON 更好排查;RAG 知识库要定期更新,历史工单积累多了检索质量会下降;Agent 的决策日志建议落库,出问题时能回溯是哪一步判断错了。
最后提醒一句,别让 Agent 直接操作生产环境的 Jira。先在测试项目里跑一周,确认决策准确率稳定了再放开权限。工具调用加上人工确认环节,比全自动更靠谱。