# 使用LangChain+LangGraph开发AI应用（1） - 项目初始化


现在做 AI 应用的人，几乎都绕不开 LangChain 和 LangGraph 这两个名字。

大模型本身只会聊天，你问一句它答一句。要把这种能力变成能干活的应用，还得给它接上工具，查数据库、调接口、读文件，还要记住多轮对话里聊过什么。流程长一点，需要能在中间停下来等人确认；跑上十分钟的任务中途挂了，得从断点续上，而不是从头再来。这些事情每件都不难，凑在一起就是一套基础设施。

LangChain 和 LangGraph 就是这套基础设施。LangChain 把模型、消息、工具收拢成统一抽象，一个 `create_agent` 就能把模型和工具组装成会自己调用的智能体。LangGraph 负责编排，把执行过程变成一张有状态的图，每一步都存档，可以暂停、恢复、回放，也能把写死的流程和模型自己决定的步骤混在一张图里。Klarna、Uber、J.P. Morgan 已经在用 LangGraph 跑生产环境的智能体流程，这是官方文档列出来的客户，它不再是演示玩具。

这个系列我一共写 12 篇，前 10 篇循序渐进讲知识点，最后 2 篇各做一个完整项目收尾，一个个人知识库，一个个人旅游助手。每篇都带可运行的源码，源码用 uv 管理，测试通过后我才提交。

第一篇只做一件事，把项目骨架立起来，让后面每一篇都有一个能跑、能测、能继续加功能的位置。

![](https://static.xiongneng.me/langchain-langgraph-cover-20261017105111.png)

## 先把生态看清楚

装包之前先花几分钟看清这四个产品的关系，不然后面很容易把框架和平台混为一谈。

LangChain 官方文档把产品分成四层。Deep Agents 是开箱即用的智能体外壳，自带上下文压缩、虚拟文件系统和子智能体能力，适合快速起步。LangChain 是智能体框架，入口是 `create_agent` 这一个函数，把模型、工具、提示和中间件组装成可定制的智能体。LangGraph 是底层编排运行时，负责有状态工作流的持久化执行、流式输出和人工介入。LangSmith 是商业平台，做追踪、评估和部署，跨框架可用，不用它也不影响框架本身工作。

![](https://static.xiongneng.me/langchain-ecosystem-20261017094317.png)

我把系列主线定在 LangChain 加 LangGraph。Deep Agents 那些高级能力后面如果用到会单独讲，LangSmith 只在需要调试时提一句，不放进主线。原因很简单，这两个是开源库，代码就在你手里，讲清楚每一行做了什么，这个系列才算数。

2025 年 10 月 LangChain 发布了 1.0 版本，这次大改动把主包收窄到只剩 `create_agent` 一个高级抽象，以前版本里的 chains、retrievers 等旧 API 挪到了 `langchain-classic` 包。LangGraph 也在差不多时候发布了 1.0。网上还有大量 0.x 时代的教学文章， import 路径和写法对不上，照着抄会报一堆 ImportError。这个系列我从头就按 1.x 的写法来。

## 工具链和版本

这个系列我用 Python 3.14，本机实际版本 3.14.3，依赖管理用 uv。命令执行结果如下。

```bash
uv --version
python --version
```

```text
uv 0.11.7 (9d177269e 2026-04-15 x86_64-pc-windows-msvc)
Python 3.14.3
```

源码我统一放在仓库的 `source` 目录，和 `articles` 平级。进去以后执行 `uv init --bare`，`--bare` 表示只生成项目描述文件，不要示例代码和 README。

```bash
mkdir source
cd source
uv init --bare --python 3.14
```

```text
Initialized project `source`
```

uv 生成的项目名取自目录名，叫 `source`。这个名字没有信息量，我把它改成 `xneng-langchain-langgraph`，和仓库名保持一致，读者在 GitHub 上看到的名字和本地项目名对得上。

```toml
[project]
name = "xneng-langchain-langgraph"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = []
```

接着装依赖。四个包，各有分工。

```bash
uv add 'langchain==1.4.3' 'langgraph==1.2.13' 'langchain-openai==1.7.2' 'pytest==9.1.1'
```

`langchain` 是智能体框架本体，`langgraph` 是编排运行时，`langchain-openai` 是 OpenAI 模型的接入包，`pytest` 跑测试。这里我把版本锁到了小版本。我给教程系列定了条硬规矩，读者按命令执行时结果要稳定。只写 `langchain>=1.4` 的话，下个月一个新版本就可能让后面的代码和截图对不上。实际产品则应该跟踪安全更新，这两件事不冲突，教程追求可复现，产品追求可维护。

写入 `pyproject.toml` 的结果如下。

```toml
[project]
name = "xneng-langchain-langgraph"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = [
    "langchain==1.4.3",
    "langchain-openai==1.7.2",
    "langgraph==1.2.13",
    "pytest==9.1.1",
]
```

`uv add` 同时生成 `uv.lock`，这个阶段共锁定 51 个包。直接依赖只有上面四个，其余都是传递依赖。其中 `langchain-core` 锁定 1.6.12，LangChain 的消息、工具、模型基类都住在这里，后面写类型注解会经常 import 它。`langsmith` 锁定 0.14.11，LangChain 的追踪数据默认发给它，没有 API key 时只会在本地静默跳过，不影响运行。

检查锁文件和 `pyproject.toml` 是否一致，用这条命令。

```bash
uv lock --check
```

```text
Resolved 51 packages in 1ms
```

`uv.lock` 比 `requirements.txt` 更适合当锁文件，因为它记录完整解析结果和哈希。以后升级依赖时，不要只看 `pyproject.toml` 的几行 diff，还要看 `uv.lock` 的 diff，一个看起来很小的声明变化可能带来一串传递依赖升级。

## 第一个 Agent

依赖装好后写第一个示例。LangChain 1.x 的入口就是 `create_agent`，它接收三样东西，模型、工具列表、系统提示，返回一个编译好的图。官方文档的快速示例是查询天气，我也从这里开始。

新建 `hello_agent.py`。

```python
"""第 1 篇示例：第一个 LangChain Agent。

用 create_agent 把「模型 + 工具 + 系统提示」组装成一个会调用工具的智能体。

模型来源：
- 设置环境变量 AGENT_MODEL（例如 openai:gpt-5.5）时使用真实模型，
  需要对应提供商的 API key；
- 未设置时使用 ScriptedChatModel 在本地演示完整的工具调用循环，
  不需要任何 API key。
"""

import os

from langchain.agents import create_agent
from langchain_core.messages import AIMessage

from fake_models import ScriptedChatModel


def get_weather(city: str) -> str:
    """查询指定城市的天气。"""
    return f"{city}今天晴天，气温 26℃，适合出行。"


def build_agent():
    """组装 Agent：优先使用环境变量配置的真实模型。"""
    model_name = os.environ.get("AGENT_MODEL")
    if model_name:
        from langchain.chat_models import init_chat_model

        model = init_chat_model(model_name)
    else:
        # 未配置真实模型时，用脚本化模型演示完整循环：
        # 第一轮模型决定调用工具，第二轮拿到工具结果后生成回答
        model = ScriptedChatModel(
            messages=iter(
                [
                    AIMessage(
                        content="",
                        tool_calls=[
                            {"name": "get_weather", "args": {"city": "北京"}, "id": "call_1"}
                        ],
                    ),
                    AIMessage(content="北京今天晴天，气温 26℃，适合出行。"),
                ]
            )
        )

    return create_agent(
        model=model,
        tools=[get_weather],
        system_prompt="你是一个简洁的天气助手，回答不超过两句话。",
    )


def main() -> None:
    agent = build_agent()
    result = agent.invoke({"messages": [{"role": "user", "content": "北京天气怎么样？"}]})

    print("== 消息流 ==")
    for message in result["messages"]:
        print(f"[{message.type}] {message.content}")
        for call in getattr(message, "tool_calls", None) or []:
            print(f"    -> 调用工具 {call['name']}({call['args']})")


if __name__ == "__main__":
    main()
```

`get_weather` 就是一个普通 Python 函数。框架从函数签名读取参数，从 docstring 生成工具描述，模型看到的就是这份描述。工具函数写错类型注解，模型调用时就会收到校验错误，这份约束是实打实的。

运行这个示例。

```bash
uv run python hello_agent.py
```

```text
== 消息流 ==
[human] 北京天气怎么样？
[ai] 
    -> 调用工具 get_weather({'city': '北京'})
[tool] 北京今天晴天，气温 26℃，适合出行。
[ai] 北京今天晴天，气温 26℃，适合出行。
```

四条消息走完了一个完整的智能体循环。用户提问后，第一轮模型没回答，输出了一次工具调用请求，参数是 `{'city': '北京'}`。框架执行 `get_weather`，把返回值作为 tool 消息追加进历史。第二轮模型读到工具结果，生成最终回答。

![](https://static.xiongneng.me/agent-loop-20261017094317.png)

这个循环会一直转到模型不再请求工具为止。中间任何一轮模型觉得信息不够，可以再调一次工具，甚至换一个工具。这就是 Agent 和普通函数调用的区别，控制流由模型在运行时决定，不用写死在代码里。

`create_agent` 内部就是把 LangGraph 的图搭好了，模型节点和工具节点交替执行。所以它返回的对象可以直接 `.invoke()`，也可以用 LangGraph 的方式 `.stream()`。讲模型抽象时，会看到这个循环的每一步都可以单独配置。

## 模型从哪来

示例里我最想解释的是 `ScriptedChatModel`。没有它，读者没有 API key 就跑不了这个程序，我的教程会断在第一篇。

真实模型的接入方式只有一行。设置环境变量，模型名按 `提供商:模型名` 的格式传给 `init_chat_model`。

```bash
# Linux / macOS
export AGENT_MODEL=openai:gpt-5.5

# Windows PowerShell
$env:AGENT_MODEL="openai:gpt-5.5"
```

OpenAI 的 API key 用 `OPENAI_API_KEY` 环境变量传入，`langchain-openai` 包会自动读取。Anthropic、Google 等提供商换成对应的接入包即可，官方集成列表里有上百个，接口都长一个样。

没有 key 的时候，示例需要一个本地替身。LangChain 自带的 `GenericFakeChatModel` 可以按脚本返回预设消息，但它没有实现 `bind_tools`，而 `create_agent` 内部一定会调用这个方法绑定工具，直接传会报 `NotImplementedError`。所以有了 `fake_models.py`，继承它补上 `bind_tools`。

```python
"""脚本化聊天模型（测试替身）。

两个用途：
1. 本地没有模型 API key 时，完整演示 Agent 的工具调用循环；
2. 作为测试替身，让单元测试不依赖外部 API，随时可跑。

GenericFakeChatModel 会按预设顺序返回消息，但它没有实现 bind_tools，
而 create_agent 内部会调用 bind_tools 绑定工具，所以这里补上。
"""

from collections.abc import Sequence

from langchain_core.language_models import GenericFakeChatModel
from langchain_core.tools import BaseTool


class ScriptedChatModel(GenericFakeChatModel):
    """按脚本依次返回消息，并支持 bind_tools 的聊天模型。"""

    def bind_tools(
        self, tools: Sequence[BaseTool], **kwargs: object
    ) -> "ScriptedChatModel":
        """create_agent 会调用该方法绑定工具，脚本化模型直接返回自身。

        是否触发工具调用由预设消息里的 tool_calls 决定，与工具绑定无关。
        """
        return self
```

这个替身我后面每篇都会用到。测试里断言工具循环是否走通，靠的就是它预设的两条消息，第一条带 `tool_calls`，第二条是最终回答。真实模型的输出有随机性，写不进断言。

## 第一个 LangGraph 应用

Agent 示例里 LangGraph 藏在框架内部。现在直接用它写一个应用，把图的概念摆到台面上。

新建 `hello_graph.py`。

```python
"""第 1 篇示例：第一个 LangGraph 应用。

用 StateGraph 定义一个只有单个节点的图：

    START -> greet -> END

节点函数读取输入消息，返回一条 AI 消息；MessagesState 自带
add_messages 归约器，节点返回的消息会追加进消息历史。
"""

from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import MessagesState


def build_graph():
    """构建并编译图：一个节点，一条直线。"""

    def greet(state: MessagesState):
        """节点函数：根据用户输入生成一句问候。"""
        user_text = state["messages"][-1].content
        reply = f"你好，我是运行在图上的第一个节点，收到了：{user_text}"
        return {"messages": [{"role": "ai", "content": reply}]}

    graph = StateGraph(MessagesState)
    graph.add_node("greet", greet)
    graph.add_edge(START, "greet")
    graph.add_edge("greet", END)
    return graph.compile()


def main() -> None:
    app = build_graph()
    result = app.invoke({"messages": [{"role": "user", "content": "很高兴认识你"}]})

    print("== 消息流 ==")
    for message in result["messages"]:
        print(f"[{message.type}] {message.content}")


if __name__ == "__main__":
    main()
```

跑一下。

```bash
uv run python hello_graph.py
```

```text
== 消息流 ==
[human] 很高兴认识你
[ai] 你好，我是运行在图上的第一个节点，收到了：很高兴认识你
```

这个图的全部内容就是三行结构加一个节点函数。`StateGraph(MessagesState)` 声明状态的类型，`MessagesState` 是框架内置的状态类，只有一个 `messages` 字段。`add_node` 注册节点，`add_edge` 连接边，`compile()` 之后才得到可执行的应用。

![](https://static.xiongneng.me/first-graph-20261017094317.png)

这里有两个细节。

节点函数收到的是完整状态，返回的是增量。`greet` 只返回一条新消息，没有返回整个历史。`MessagesState` 的 `messages` 字段自带 `add_messages` 归约器，节点返回的消息会追加到列表尾部，而不是覆盖。后面的篇章会频繁和归约器打交道，累加、覆盖、去重，都靠它。

`START` 和 `END` 是框架内置节点，不是字符串占位。每条边都必须是确定的目标，图编译时会检查所有节点可达。现在这张图只有一条路，第 8 篇讲条件路由时会看到从一个节点分出多条边，靠状态值决定走哪条。

## 用测试锁住骨架

项目刚建好，我想让测试回答三个问题。工具函数的行为是否符合预期，Agent 的工具循环是否走通，图是否从 START 走到 END。

新建 `tests/test_hello_agent.py`。

```python
"""第 1 篇示例测试：Agent 的工具调用循环。"""

from hello_agent import build_agent, get_weather


def test_get_weather():
    """工具函数返回包含城市名与天气描述的字符串。"""
    assert get_weather("北京") == "北京今天晴天，气温 26℃，适合出行。"


def test_agent_runs_full_tool_call_loop(monkeypatch):
    """无 AGENT_MODEL 时，Agent 用脚本化模型走完整循环：

    模型请求调用工具 -> 工具执行 -> 模型生成最终回答。
    """
    monkeypatch.delenv("AGENT_MODEL", raising=False)

    agent = build_agent()
    result = agent.invoke({"messages": [{"role": "user", "content": "北京天气怎么样？"}]})

    # 工具执行结果进入消息历史
    tool_messages = [m for m in result["messages"] if m.type == "tool"]
    assert len(tool_messages) == 1
    assert "晴天" in tool_messages[0].content

    # 最后一条是模型读完工具结果后的回答
    assert result["messages"][-1].type == "ai"
    assert "适合出行" in result["messages"][-1].content
```

第二条测试是整个仓库的第一道保险。它不断言模型说了什么，那由真实模型决定，它断言的是结构，消息历史里必须出现一条 tool 消息，最后一条必须是 ai。哪天升级 LangChain 后工具循环的实现变了，这条测试会先失败。

`monkeypatch.delenv` 删掉 `AGENT_MODEL`，保证测试环境一定走脚本化模型。不然测试在配了 key 的机器上会去打真实 API，又慢又花钱。

再新建 `tests/test_hello_graph.py`。

```python
"""第 1 篇示例测试：单节点图的执行。"""

from hello_graph import build_graph


def test_graph_runs_from_start_to_end():
    """图从 START 走到 END，用户消息在前，AI 回答在后。"""
    app = build_graph()
    result = app.invoke({"messages": [{"role": "user", "content": "很高兴认识你"}]})

    assert result["messages"][0].type == "human"
    assert result["messages"][-1].type == "ai"
    assert "第一个节点" in result["messages"][-1].content
```

pytest 默认把测试文件所在目录加进导入路径，`tests` 不在 `source` 根下，`from hello_agent import ...` 会找不到模块。在 `pyproject.toml` 里加两行，把项目根加进 `pythonpath`。

```toml
[tool.pytest.ini_options]
pythonpath = ["."]
```

现在跑测试。

```bash
uv run pytest -v
```

```text
platform win32 -- Python 3.14.3, pytest-9.1.1, pluggy-1.7.0
rootdir: D:\projects\AI\xneng-langchain-langgraph\source
configfile: pyproject.toml
plugins: anyio-4.15.1, langsmith-0.14.11
collected 3 items

tests/test_hello_agent.py::test_get_weather PASSED                       [ 33%]
tests/test_hello_agent.py::test_agent_runs_full_tool_call_loop PASSED    [ 66%]
tests/test_hello_graph.py::test_graph_runs_from_start_to_end PASSED      [100%]

============================== 3 passed in 1.06s ==============================
```

三条全绿。我把这个阶段的验证命令整理成四条，你克隆仓库后依次执行，就能确认环境正常。

```bash
uv sync
uv lock --check
uv run python hello_agent.py
uv run pytest
```

## 生成物不该进入 Git

`source` 目录里现在有 `.venv` 和 `.uv-cache`，运行测试还会生成 `__pycache__` 和 `.pytest_cache`。这些对本机开发有用，对版本控制没有意义，`.venv` 里的文件带绝对路径，换台机器就不能用。

仓库根目录的 `.gitignore` 已经覆盖了这些情况。

```gitignore
.uv-cache/
.uv-python/
.venv/
venv/
__pycache__/
.pytest_cache/
.env
.env.*
```

`.env` 一定要排掉。OSS 密钥、模型 API key 都在里面，误提交一次就得全部轮换。我放了一份 `.env.example` 在项目里，只留键名不留值，告诉读者需要配什么。

## 提交

仓库的第一笔提交是基线，放任务说明书、参考文章和辅助脚本。项目代码从第二笔提交开始单独推进，每篇对应一笔，读者看到某一篇时，能通过对应提交回到当时的源码状态。

```bash
git status --short
git add source articles
git commit -m 'feat(stage-01): 01 项目初始化'
```

提交成功后，版本库里新增 9 个文件，文章、源码、锁定文件和两个测试文件都在里面。`git log --oneline` 能看到两笔提交，基线一笔，本篇一笔。

```text
feat(stage-01): 01 项目初始化
chore: 初始化仓库基线
```

现在 `source` 目录是这样。

```text
source/
├── fake_models.py           # 脚本化模型，无 key 演示与测试用
├── hello_agent.py           # 第一个 Agent
├── hello_graph.py           # 第一个 LangGraph 应用
├── pyproject.toml           # 项目与依赖声明
├── uv.lock                  # 锁定文件
├── README.md                # 阶段说明
└── tests/
    ├── test_hello_agent.py
    └── test_hello_graph.py
```

第一篇做完，仓库有了明确的运行时，Python 3.14、LangChain 1.4.3、LangGraph 1.2.13，依赖锁死，三条测试，两个能跑的示例。后面每加一个知识点，都可以重复这套流程，写示例，跑起来，加测试，提交。

最后补一句我对版本的取舍。教程锁小版本是为了可复现，实际项目应该跟着安全更新走，升级前读读 release notes，看看不兼容变化，再跑一遍这个仓库的测试。测试先绿，再考虑合并。

## 源码

[https://github.com/yidao620c/xneng-langchain-langgraph](https://github.com/yidao620c/xneng-langchain-langgraph)

## 参考

- [LangChain 官方文档](https://docs.langchain.com/oss/python/langchain/overview)
- [LangGraph 官方文档](https://docs.langchain.com/oss/python/langgraph/overview)
- [LangChain 安装指南](https://docs.langchain.com/oss/python/langchain/install)


---

> 作者: XiongNeng  
> URL: https://xiongneng.me/posts/ai/langchain/langchain-langgraph-01-init/  

