先运行,再讲解
第 1 节:完整项目代码与运行
这节先不拆概念。下载 ZIP,按页面上的两条命令启动;接下来再读代码。
1. 下载和启动
pip install -r requirements.txt
python main.py在解压后的项目目录执行。输入:小米集团最近有什么值得关注的信息?。默认走本地模拟工具,因此无需 API Key、行情 API 或联网搜索服务。
2. 要用真实模型时,直接改代码顶部
打开 main.py,填写服务商发放的密钥,设置模型名;把 MOCK_MODE 改为 False。兼容 OpenAI 的服务按服务商说明填写 BASE_URL。
MOCK_MODE = False
OPENAI_API_KEY = "在这里填写你自己的 API Key"
OPENAI_MODEL = "gpt-4o-mini"
OPENAI_BASE_URL = ""不要把填写真实密钥的文件提交到公共 Git 仓库。没有密钥就保持模拟模式。
3. 完整代码:main.py
以下代码和下载 ZIP 中的 main.py 一致,复制时保留完整内容。依赖版本在 ZIP 的 requirements.txt。
"""ApacheCN 教学示例:LangChain + LangGraph 投研工作流。
示例工具只返回虚构模拟数据,不提供投资建议。
"""
import os
import sys
from typing import Literal, TypedDict
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langchain_core.prompts import ChatPromptTemplate
from langgraph.graph import END, START, StateGraph
from pydantic import BaseModel, Field
# ===== 在这里配置 =====
MOCK_MODE = True # 不配置密钥先保持 True;要调用模型时改为 False
OPENAI_API_KEY = "" # 填入服务商发放的 API Key;不要提交含真实密钥的代码
OPENAI_MODEL = "gpt-4o-mini"
OPENAI_BASE_URL = "" # OpenAI 兼容服务可填写服务商提供的地址;官方接口留空
# =====================
if OPENAI_API_KEY:
os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY
class ResearchQuery(BaseModel):
"""从用户问题抽取研究计划,不确定的证券代码留空。"""
company: str | None = Field(default=None, description="公司或证券名称")
market: Literal["CN", "HK", "US", "unknown"] = "unknown"
symbol: str | None = Field(default=None, description="证券代码;无法确认时为 null")
needs_quote: bool = Field(default=False, description="是否询问行情")
needs_search: bool = Field(default=False, description="是否需要最新资料")
class AgentState(TypedDict, total=False):
question: str
query: dict
quote: dict | None
sources: list[dict]
answer: str
errors: list[str]
cache_hit: bool
@tool
def mock_quote(company: str) -> dict:
"""获取本教程内置的模拟行情,绝非实时数据。"""
return {
"label": "模拟行情",
"company": company,
"price": 68.50,
"change_percent": 1.25,
"as_of": "虚构示例日期",
"source": "本地示例数据",
}
@tool
def mock_search(company: str) -> list[dict]:
"""查询本教程内置的示例资料,不会访问互联网。"""
if "无结果" in company:
return []
return [{
"label": "模拟资料",
"title": f"{company}:用于演示资料引用的虚构条目",
"url": "https://example.invalid/mock-source",
"summary": "这是程序内置的示例,不是新闻或真实公司信息。",
}]
def make_model():
if MOCK_MODE:
return None
if not OPENAI_API_KEY:
raise ValueError("请在文件顶部填写 OPENAI_API_KEY,或将 MOCK_MODE 设为 True。")
kwargs = {"temperature": 0}
if OPENAI_BASE_URL:
kwargs["base_url"] = OPENAI_BASE_URL
return init_chat_model(f"openai:{OPENAI_MODEL}", **kwargs)
QUERY_PROMPT = ChatPromptTemplate.from_messages([
("system", "只提取用户明确提到的公司信息,不猜证券代码。判断是否需要行情或最新资料。"),
("human", "{question}"),
])
ANSWER_PROMPT = ChatPromptTemplate.from_messages([
("system", "用中文回答。仅依据给定资料陈述事实;资料不足就明确说明。模拟数据必须标注为模拟,不提供投资建议。"),
("human", "问题:{question}\n解析:{query}\n行情:{quote}\n资料:{sources}"),
])
def parse_question(state: AgentState) -> dict:
question = state["question"]
if MOCK_MODE:
company = next((name for name in ("小米集团", "腾讯", "阿里巴巴", "比亚迪") if name in question), None)
needs_quote = any(word in question for word in ("股价", "行情", "价格"))
needs_search = any(word in question for word in ("最近", "新闻", "信息", "关注"))
market = "HK" if company in ("小米集团", "腾讯", "阿里巴巴") else "CN" if company else "unknown"
parsed = ResearchQuery(company=company, market=market, needs_quote=needs_quote, needs_search=needs_search)
else:
model = make_model()
parsed = (QUERY_PROMPT | model.with_structured_output(ResearchQuery)).invoke({"question": question})
return {"query": parsed.model_dump()}
def route(state: AgentState) -> str:
if state.get("cache_hit"):
return "answer"
query = state.get("query", {})
if not query.get("company"):
return "answer"
if query.get("needs_quote"):
return "quote"
if query.get("needs_search"):
return "search"
return "answer"
def check_cache(state: AgentState) -> dict:
cached = ANSWER_CACHE.get(state["question"])
return {"cache_hit": cached is not None, "answer": cached}
def after_quote(state: AgentState) -> str:
return "search" if state.get("query", {}).get("needs_search") else "answer"
def get_quote(state: AgentState) -> dict:
try:
return {"quote": mock_quote.invoke({"company": state["query"]["company"]})}
except Exception as exc:
return {"quote": None, "errors": [f"行情工具异常:{exc}"]}
def search(state: AgentState) -> dict:
try:
rows = mock_search.invoke({"company": f"{state['query']['company']} {state['question']}"})
return {"sources": rows}
except Exception as exc:
return {"sources": [], "errors": [f"搜索工具异常:{exc}"]}
def answer(state: AgentState) -> dict:
if state.get("cache_hit") and state.get("answer"):
return {"answer": state["answer"]}
query = state.get("query", {})
quote = state.get("quote")
sources = state.get("sources", [])
if not query.get("company"):
text = "我还不能确定你问的是哪家公司,请补充公司全名或证券名称。"
elif not quote and not sources:
text = "没有取得可核实的资料,因此无法作事实判断。请补充信息或稍后重试。"
elif MOCK_MODE:
text = (
f"识别结果:{query['company']}(市场:{query['market']})。\n"
f"行情:{quote or '本次未查询行情'}\n资料:{sources or '本次未查询资料'}\n"
"以上全部是本地虚构模拟数据,不是实时行情或新闻,也不构成投资建议。"
)
else:
try:
text = (ANSWER_PROMPT | make_model()).invoke({
"question": state["question"], "query": query,
"quote": quote, "sources": sources,
}).content
except Exception as exc:
text = f"模型调用失败,当前无法形成回答:{exc}"
ANSWER_CACHE[state["question"]] = text
return {"answer": text}
def build_graph():
graph = StateGraph(AgentState)
graph.add_node("parse", parse_question)
graph.add_node("cache", check_cache)
graph.add_node("quote", get_quote)
graph.add_node("search", search)
graph.add_node("answer", answer)
graph.add_edge(START, "parse")
graph.add_edge("parse", "cache")
graph.add_conditional_edges("cache", route, {
"quote": "quote", "search": "search", "answer": "answer",
})
graph.add_conditional_edges("quote", after_quote, {"search": "search", "answer": "answer"})
graph.add_edge("search", "answer")
graph.add_edge("answer", END)
return graph.compile()
ANSWER_CACHE: dict[str, str] = {}
def main():
question = " ".join(sys.argv[1:]).strip() or input("你想了解哪家公司? ").strip()
if not question:
print("请输入一个问题。")
return
try:
result = build_graph().invoke({"question": question, "sources": [], "errors": []})
print("\n" + result.get("answer", "没有生成回答。"))
for error in result.get("errors", []):
print("处理记录:" + error)
except Exception as exc:
print(f"运行失败:{exc}")
if __name__ == "__main__":
main()
4. 预期结果和边界
程序输出识别公司、市场和模拟资料,并醒目标出这些是虚构示例。输入“请查无结果测试公司的新闻”会得到资料不足说明;不能识别公司时会请用户补充名称。
这里没有接真实行情或搜索服务,示例数值不能用于任何投资判断。