设置
本教程使用 LangGraph 进行智能体编排,使用 OpenAI 的 GPT-4o、Tavily 进行搜索,使用 E2B 的 代码解释器,以及使用 Polygon 来检索股票数据,但只需稍作修改即可适用于其他框架、模型和工具。Tavily、E2B 和 Polygon 均可免费注册。安装
首先,安装构建智能体所需的软件包:pip install -U langgraph langchain[openai] langchain-community e2b-code-interpreter
yarn add @langchain/openai @langchain/community @langchain/langgraph @langchain/core @e2b/code-interpreter @polygon.io/client-js openai zod
# Make sure you have langsmith>=0.3.1
pip install -U "langsmith[pytest]"
yarn add -D langsmith vitest
yarn add -D langsmith jest
环境变量
设置以下环境变量:export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=<YOUR_LANGSMITH_API_KEY>
export OPENAI_API_KEY=<YOUR_OPENAI_API_KEY>
export TAVILY_API_KEY=<YOUR_TAVILY_API_KEY>
export E2B_API_KEY=<YOUR_E2B_API_KEY>
export POLYGON_API_KEY=<YOUR_POLYGON_API_KEY>
创建您的应用
要定义我们的 ReAct 智能体,我们将使用 LangGraph/LangGraph.js 进行编排,并使用 LangChain 来集成 LLM 和工具。定义工具
首先,我们将定义智能体中要使用的工具。将会有 3 个工具:- 使用 Tavily 的搜索工具
- 使用 E2B 的代码解释器工具
- 使用 Polygon 的股票信息工具
The langchain-community package is no longer maintained. Examples that import from langchain_community may be outdated or broken. Use with caution.from langchain_community.tools import TavilySearchResults
from e2b_code_interpreter import Sandbox
from langchain_community.tools.polygon.aggregates import PolygonAggregates
from langchain_community.utilities.polygon import PolygonAPIWrapper
from typing_extensions import Annotated, TypedDict, Optional, Literal
# 定义搜索工具
search_tool = TavilySearchResults(
max_results=5,
include_raw_content=True,
)
# 定义代码工具
def code_tool(code: str) -> str:
"""执行 Python 代码并返回结果。"""
sbx = Sandbox()
execution = sbx.run_code(code)
if execution.error:
return f"错误: {execution.error}"
return f"结果: {execution.results}, 日志: {execution.logs}"
# 定义股票行情代码工具的输入模式
class TickerToolInput(TypedDict):
"""行情代码工具的输入格式。
该工具将从 from_date 到 to_date 拉取聚合数据块(timespan_multiplier * timespan)。
"""
ticker: Annotated[str, ..., "股票的行情代码"]
timespan: Annotated[Literal["minute", "hour", "day", "week", "month", "quarter", "year"], ..., "时间窗口的大小。"]
timespan_multiplier: Annotated[int, ..., "时间窗口的倍数"]
from_date: Annotated[str, ..., "开始拉取数据的日期,YYYY-MM-DD 格式 - 仅包含年、月、日"]
to_date: Annotated[str, ..., "停止拉取数据的日期,YYYY-MM-DD 格式 - 仅包含年、月、日"]
api_wrapper = PolygonAPIWrapper()
polygon_aggregate = PolygonAggregates(api_wrapper=api_wrapper)
# 定义股票行情代码工具
def ticker_tool(query: TickerToolInput) -> str:
"""为行情代码拉取数据。"""
return polygon_aggregate.invoke(query)
The
@langchain/community package is no longer maintained. Examples that import from @langchain/community may be outdated or broken. Use with caution.import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
import { Sandbox } from "@e2b/code-interpreter";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { restClient } from "@polygon.io/client-js";
// 定义搜索工具
const searchTool = new TavilySearchResults({
maxResults: 5,
});
// 定义代码工具
const codeTool = tool(async (input) => {
const sbx = await Sandbox.create();
const execution = await sbx.runCode(input.code);
if (execution.error) {
return `错误: ${execution.error}`;
}
return `结果: ${execution.results}, 日志: ${execution.logs}`;
}, {
name: "code",
description: "执行 Python 代码并返回结果。",
schema: z.object({
code: z.string().describe("要执行的 Python 代码"),
}),
});
// 定义股票行情代码工具的输入模式
const TickerToolInputSchema = z.object({
ticker: z.string().describe("股票的行情代码"),
timespan: z.enum(["minute", "hour", "day", "week", "month", "quarter", "year"]).describe("时间窗口的大小。"),
timespan_multiplier: z.number().describe("时间窗口的倍数"),
from_date: z
.string()
.describe("开始拉取数据的日期,YYYY-MM-DD 格式 - 仅包含年、月、日"),
to_date: z
.string()
.describe("停止拉取数据的日期,YYYY-MM-DD 格式 - 仅包含年、月、日"),
});
const rest = restClient(process.env.POLYGON_API_KEY);
// 定义股票行情代码工具
const tickerTool = tool(async (query) => {
const parsed = TickerToolInputSchema.parse(query);
const result = await rest.stocks.aggregates(
parsed.ticker,
parsed.timespan_multiplier,
parsed.timespan,
parsed.from_date,
parsed.to_date
);
return JSON.stringify(result);
}, {
name: "ticker",
description: "为行情代码拉取数据",
schema: TickerToolInputSchema,
});
定义智能体
现在我们已经定义了所有工具,可以使用create_agent 来创建我们的智能体。
from typing_extensions import Annotated, TypedDict
from langchain.agents import create_agent
class AgentOutputFormat(TypedDict):
numeric_answer: Annotated[float | None, ..., "数值答案,如果用户要求的话"]
text_answer: Annotated[str | None, ..., "文本答案,如果用户要求的话"]
reasoning: Annotated[str, ..., "答案背后的推理过程"]
agent = create_agent(
model="gpt-5.4-mini",
tools=[code_tool, search_tool, polygon_aggregates],
response_format=AgentOutputFormat,
system_prompt="你是一位金融专家。准确回答用户的查询。",
)
import { z } from "zod";
import { ChatOpenAI } from "@langchain/openai";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
const AgentOutputFormatSchema = z.object({
numeric_answer: z.number().optional().describe("数值答案,如果用户要求的话"),
text_answer: z.string().optional().describe("文本答案,如果用户要求的话"),
reasoning: z.string().describe("答案背后的推理过程"),
})
const tools = [codeTool, searchTool, tickerTool];
const agent = createReactAgent({
llm: new ChatOpenAI({ model: "gpt-5.5" }),
tools: tools,
responseFormat: AgentOutputFormatSchema,
stateModifier: "你是一位金融专家。准确回答用户的查询。",
});
export default agent;
编写测试
现在我们已经定义了智能体,让我们编写一些测试来确保基本功能。在本教程中,我们将测试智能体的工具调用能力是否正常工作,智能体是否知道忽略不相关的问题,以及它是否能够回答涉及使用所有工具的复杂问题。 我们首先需要设置一个测试文件,并在文件顶部添加所需的导入。创建一个 `tests/test_agent.py` 文件。
from app import agent, polygon_aggregates, search_tool # 从你定义智能体的地方导入
import pytest
from langsmith import testing as t
将你的测试文件命名为 `agent.vitest.eval.ts`
import { expect } from "vitest";
import * as ls from "langsmith/vitest";
import agent from "../agent"; // 从你定义智能体的地方导入
// 可选,但建议将测试分组在一起
ls.describe("智能体测试", () => {
// 在此放置测试
});
将你的测试文件命名为 `agent.jest.eval.ts`
import { expect } from "@jest/globals";
import * as ls from "langsmith/jest";
import agent from "../agent"; // 从你定义智能体的地方导入
// 可选,但建议将测试分组在一起
ls.describe("智能体测试", () => {
// 在此放置测试
});
测试 1:处理离题问题
第一个测试将是一个简单的检查,确保智能体在不相关的查询上不使用工具。@pytest.mark.langsmith
@pytest.mark.parametrize(
# <-- 仍然可以使用所有正常的 pytest 标记
"query",
["你好!", "你好吗?"],
)
def test_no_tools_on_offtopic_query(query: str) -> None:
"""测试智能体在离题查询上不使用工具。"""
# 记录测试样例
t.log_inputs({"query": query})
expected = []
t.log_reference_outputs({"tool_calls": expected})
# 直接调用智能体的模型节点,而不是运行 ReACT 循环。
result = agent.nodes["agent"].invoke(
{"messages": [{"role": "user", "content": query}]}
)
actual = result["messages"][0].tool_calls
t.log_outputs({"tool_calls": actual})
# 检查是否没有进行工具调用。
assert actual == expected
ls.test.each([
{ inputs: { query: "你好!" }, expected: { numMessages: 2 } },
{ inputs: { query: "你好吗?" }, expected: { numMessages: 2 } },
])(
"在离题查询上不应使用工具: %s",
async ({ inputs: { query }, expected: { numMessages } }) => {
const result = await agent.invoke({ messages: [{ role: "user", content: query }] });
ls.logOutputs(result);
// 检查流程是 人类 -> AI 最终响应(没有调用工具)
expect(result.messages).toHaveLength(numMessages);
}
);
ls.test.each([
{ inputs: { query: "你好!" }, expected: { numMessages: 2 } },
{ inputs: { query: "你好吗?" }, expected: { numMessages: 2 } },
])(
"在离题查询上不应使用工具: %s",
async ({ inputs: { query }, expected: { numMessages } }) => {
const result = await agent.invoke({ messages: [{ role: "user", content: query }] });
ls.logOutputs(result);
// 检查流程是 人类 -> AI 最终响应(没有调用工具)
expect(result.messages).toHaveLength(numMessages);
}
);
测试 2:简单的工具调用
对于工具调用,我们将验证智能体是否使用正确的参数调用了正确的工具。@pytest.mark.langsmith
def test_searches_for_correct_ticker() -> None:
"""测试模型在简单查询中查找正确的股票代码。"""
# 记录测试样例
query = "苹果的股价是多少?"
t.log_inputs({"query": query})
expected = "AAPL"
t.log_reference_outputs({"ticker": expected})
# 直接调用智能体的模型节点,而不是运行完整的 ReACT 循环。
result = agent.nodes["agent"].invoke(
{"messages": [{"role": "user", "content": query}]}
)
tool_calls = result["messages"][0].tool_calls
if tool_calls[0]["name"] == polygon_aggregates.name:
actual = tool_calls[0]["args"]["ticker"]
else:
actual = None
t.log_outputs({"ticker": actual})
# 检查查询了正确的股票代码
assert actual == expected
ls.test(
"应搜索正确的股票代码",
{
inputs: { query: "苹果的股价是多少?" },
expected: { numMessages: 4 },
},
async ({ inputs: { query }, expected: { numMessages } }) => {
const result = await agent.invoke({
messages: [{ role: "user", content: query }],
});
ls.logOutputs(result);
// 智能体应该对 ticker 工具进行了一次调用
const toolCalls = (result.messages[1] as AIMessage).tool_calls || [];
const tickerQuery = JSON.parse(toolCalls[0].function.arguments).query.ticker;
// 检查查询了正确的股票代码
expect(tickerQuery).toBe("AAPL");
// 检查流程是 人类 -> AI -> 工具 -> AI 最终响应
expect(result.messages).toHaveLength(numMessages);
}
);
ls.test(
"应搜索正确的股票代码",
{
inputs: { query: "苹果的股价是多少?" },
expected: { numMessages: 4 },
},
async ({ inputs: { query }, expected: { numMessages } }) => {
const result = await agent.invoke({
messages: [{ role: "user", content: query }],
});
ls.logOutputs(result);
// 智能体应该对 ticker 工具进行了一次调用
const toolCalls = (result.messages[1] as AIMessage).tool_calls || [];
const tickerQuery = JSON.parse(toolCalls[0].function.arguments).query.ticker;
// 检查查询了正确的股票代码
expect(tickerQuery).toBe("AAPL");
// 检查流程是 人类 -> AI -> 工具 -> AI 最终响应
expect(result.messages).toHaveLength(numMessages);
}
);
测试 3:复杂的工具调用
有些工具调用比其他的更容易测试。对于股票代码查询,我们可以断言查询了正确的代码。对于编码工具,其输入和输出的约束要少得多,而且有很多方法可以得到正确的答案。在这种情况下,通过运行完整的智能体并断言它既调用了编码工具又得到了正确答案来测试工具是否正确使用会更简单。@pytest.mark.langsmith
def test_executes_code_when_needed() -> None:
query = (
"过去一年中,Facebook 股价上涨了 66.76%,"
"苹果上涨了 25.24%,谷歌上涨了 37.11%,亚马逊上涨了 47.52%,"
"Netflix 上涨了 78.31%。FAANG 股票过去一年的平均回报率"
"是多少,以百分比表示?"
)
t.log_inputs({"query": query})
expected = 50.988
t.log_reference_outputs({"response": expected})
# 测试智能体在需要时执行代码
result = agent.invoke({"messages": [{"role": "user", "content": query}]})
t.log_outputs({"result": result["structured_response"].get("numeric_answer")})
# 获取 LLM 进行的所有工具调用
tool_calls = [
tc["name"]
for msg in result["messages"]
for tc in getattr(msg, "tool_calls", [])
]
# 这将记录智能体采取的步骤数,这对于确定智能体获得答案的效率很有用。
t.log_feedback(key="num_steps", score=len(result["messages"]) - 1)
# 断言使用了代码工具
assert "code_tool" in tool_calls
# 断言提供了数值答案:
assert result["structured_response"].get("numeric_answer") is not None
# 断言答案正确
assert abs(result["structured_response"]["numeric_answer"] - expected) <= 0.01
ls.test(
"在需要时应执行代码",
{
inputs: { query: "2024年 FAANG 股票的平均回报率是多少?" },
expected: { answer: 53 },
},
async ({ inputs: { query }, expected: { answer } }) => {
const result = await agent.invoke({
messages: [{ role: "user", content: query }],
});
ls.logOutputs(result);
// 获取 LLM 进行的所有工具调用
const toolCalls = result.messages
.filter(m => (m as AIMessage).tool_calls)
.flatMap(m => (m as AIMessage).tool_calls?.map(tc => tc.name));
// 这将记录 LLM 采取的步骤数,我们可以随时间跟踪它以衡量性能
ls.logFeedback({
key: "num_steps",
score: result.messages.length - 1, // 第一条消息是用户消息
});
// 断言工具调用包含 "code_tool" 函数
expect(toolCalls).toContain("code_tool");
// 断言答案在预期答案的 1 个单位之内
expect(Math.abs(result.structured_response.numeric_answer - answer)).toBeLessThanOrEqual(1);
}
);
ls.test(
"在需要时应执行代码",
{
inputs: { query: "2024年 FAANG 股票的平均回报率是多少?" },
expected: { answer: 53 },
},
async ({ inputs: { query }, expected: { answer } }) => {
const result = await agent.invoke({
messages: [{ role: "user", content: query }],
});
ls.logOutputs(result);
// 获取 LLM 进行的所有工具调用
const toolCalls = result.messages
.filter(m => (m as AIMessage).tool_calls)
.flatMap(m => (m as AIMessage).tool_calls?.map(tc => tc.name));
// 这将记录 LLM 采取的步骤数,我们可以随时间跟踪它以衡量性能
ls.logFeedback({
key: "num_steps",
score: result.messages.length - 1, // 第一条消息是用户消息
});
// 断言工具调用包含 "code_tool" 函数
expect(toolCalls).toContain("code_tool");
// 断言答案在预期答案的 1 个单位之内
expect(Math.abs(result.structured_response.numeric_answer - answer)).toBeLessThanOrEqual(1);
}
);
测试 4:LLM 作为评判者
我们将通过运行 LLM 作为评判者的评估来确保智能体的答案基于搜索结果。为了将 LLM 作为评判者的调用与我们的智能体分开跟踪,我们将在 Python 中使用 LangSmith 提供的trace_feedback 上下文管理器,在 JS/TS 中使用 wrapEvaluator 函数。
from typing_extensions import Annotated, TypedDict
from langchain.chat_models import init_chat_model
class Grade(TypedDict):
"""评估答案在源文档中的根基性。"""
score: Annotated[
bool,
...,
"如果答案完全基于源文档,则返回 True,否则返回 False。",
]
judge_llm = init_chat_model("gpt-5.5").with_structured_output(Grade)
@pytest.mark.langsmith
def test_grounded_in_source_info() -> None:
"""测试响应基于工具输出。"""
query = "根据分析师的说法,英伟达股票在 2024 年表现如何?"
t.log_inputs({"query": query})
result = agent.invoke({"messages": [{"role": "user", "content": query}]})
# 获取 LLM 进行的所有搜索调用
search_results = "\n\n".join(
msg.content
for msg in result["messages"]
if msg.type == "tool" and msg.name == search_tool.name
)
t.log_outputs(
{
"response": result["structured_response"].get("text_answer"),
"search_results": search_results,
}
)
# 将反馈 LLM 运行单独跟踪,与部署运行分开。
with t.trace_feedback():
# LLM 评判者的指令
instructions = (
"对以下答案进行评分。"
"答案应完全基于(即由源文档支持)源文档。"
"如果答案完全基于文档,则返回 True。"
"如果答案未基于文档,则返回 False。"
)
answer_and_docs = (
f"答案: {result['structured_response'].get('text_answer', '')}\n"
f"文档:\n{search_results}"
)
# 运行评判 LLM
grade = judge_llm.invoke(
[
{"role": "system", "content": instructions},
{"role": "user", "content": answer_and_docs},
]
)
t.log_feedback(key="groundedness", score=grade["score"])
assert grade['score']
// 此代码在测试之外 - 仅作为辅助函数
const judgeLLM = new ChatOpenAI({ model: "gpt-5.5" });
const groundedEvaluator = async (params: {
answer: string;
referenceDocuments: string,
}) => {
// LLM 评判者的指令
const instructions = [
"如果答案基于文档,则返回 1",
"如果答案未基于文档,则返回 0",
].join("\n");
// 运行评判 LLM
const grade = await judgeLLM.invoke([
{ role: "system", content: instructions },
{ role: "user", content: `答案: ${params.answer}\n文档: ${params.referenceDocuments}` },
]);
const score = parseInt(grade.content.toString());
return { key: "groundedness", score };
};
// 此代码在测试内部
ls.test(
"基于来源",
{
inputs: { query: "根据分析师的说法,英伟达股票在 2024 年表现如何?" },
},
async ({ inputs: { query } }) => {
const result = await agent.invoke({
messages: [{ role: "user", content: query }],
});
const wrappedEvaluator = ls.wrapEvaluator(groundedEvaluator);
await wrappedEvaluator({
answer: result.structuredResponse.text_answer ?? "",
referenceDocuments: result.structuredResponse.reasoning,
})
ls.logOutputs(result);
}
);
// 此代码在测试之外 - 仅作为辅助函数
const judgeLLM = new ChatOpenAI({ model: "gpt-5.5" });
const groundedEvaluator = async (params: {
answer: string;
referenceDocuments: string,
}) => {
// LLM 评判者的指令
const instructions = [
"如果答案基于文档,则返回 1",
"如果答案未基于文档,则返回 0",
].join("\n");
// 运行评判 LLM
const grade = await judgeLLM.invoke([
{ role: "system", content: instructions },
{ role: "user", content: `答案: ${params.answer}\n文档: ${params.referenceDocuments}` },
]);
const score = parseInt(grade.content.toString());
return { key: "groundedness", score };
};
// 此代码在测试内部
ls.test(
"基于来源",
{
inputs: { query: "根据分析师的说法,英伟达股票在 2024 年表现如何?" },
},
async ({ inputs: { query } }) => {
const result = await agent.invoke({
messages: [{ role: "user", content: query }],
});
const wrappedEvaluator = ls.wrapEvaluator(groundedEvaluator);
await wrappedEvaluator({
answer: result.structuredResponse.text_answer ?? "",
referenceDocuments: result.structuredResponse.reasoning,
})
ls.logOutputs(result);
}
);
运行测试
设置好配置文件(如果使用 Vitest 或 Jest)后,您可以使用以下命令运行测试:Vitest/Jest 的配置文件
Vitest/Jest 的配置文件
创建一个 `ls.vitest.config.ts` 文件:
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["**/*.eval.?(c|m)[jt]s"],
reporters: ["langsmith/vitest/reporter"],
setupFiles: ["dotenv/config"],
testTimeout: 30000,
},
});
创建一个 `ls.jest.config.ts` 文件:
require('dotenv').config();
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: [
'<rootDir>/tests/jest/**/*.jest.eval.ts'
],
testPathIgnorePatterns: [
'<rootDir>/tests/vitest/.*.vitest.eval.ts$'
],
reporters: ["langsmith/jest/reporter"],
testTimeout: 30000,
};
pytest --langsmith-output tests
yarn vitest --config ls.vitest.config.ts
yarn jest --config ls.jest.config.ts
参考代码
记得也要将 Vitest 和 Jest 的配置文件添加到您的项目中。智能体
智能体代码
智能体代码
The langchain-community package is no longer maintained. Examples that import from langchain_community may be outdated or broken. Use with caution.from e2b_code_interpreter import Sandbox
from langchain_community.tools import PolygonAggregates, TavilySearchResults
from langchain_community.utilities.polygon import PolygonAPIWrapper
from langchain.agents import create_agent
from typing_extensions import Annotated, TypedDict
search_tool = TavilySearchResults(
max_results=5,
include_raw_content=True,
)
def code_tool(code: str) -> str:
"""执行 Python 代码并返回结果。"""
sbx = Sandbox()
execution = sbx.run_code(code)
if execution.error:
return f"错误: {execution.error}"
return f"结果: {execution.results}, 日志: {execution.logs}"
polygon_aggregates = PolygonAggregates(api_wrapper=PolygonAPIWrapper())
class AgentOutputFormat(TypedDict):
numeric_answer: Annotated[
float | None, ..., "数值答案,如果用户要求的话"
]
text_answer: Annotated[
str | None, ..., "文本答案,如果用户要求的话"
]
reasoning: Annotated[str, ..., "答案背后的推理过程"]
agent = create_agent(
model="gpt-5.4-mini",
tools=[code_tool, search_tool, polygon_aggregates],
response_format=AgentOutputFormat,
system_prompt="你是一位金融专家。准确回答用户的查询。",
)
The
@langchain/community package is no longer maintained. Examples that import from @langchain/community may be outdated or broken. Use with caution.import { ChatOpenAI } from "@langchain/openai";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
import { Sandbox } from '@e2b/code-interpreter'
import { restClient } from '@polygon.io/client-js';
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const codeTool = tool(async (input) => {
const sbx = await Sandbox.create();
const execution = await sbx.runCode(input.code);
if (execution.error) {
return `错误: ${execution.error}`;
}
return `结果: ${execution.results}, 日志: ${execution.logs}`;
}, {
name: "code",
description: "执行 Python 代码并返回结果。",
schema: z.object({
code: z.string().describe("要执行的 Python 代码"),
}),
});
const TickerToolInputSchema = z.object({
ticker: z.string().describe("股票的行情代码"),
timespan: z.enum(["minute", "hour", "day", "week", "month", "quarter", "year"]).describe("时间窗口的大小。"),
timespan_multiplier: z.number().describe("时间窗口的倍数"),
from_date: z
.string()
.describe("开始拉取数据的日期,YYYY-MM-DD 格式 - 仅包含年、月、日"),
to_date: z
.string()
.describe("停止拉取数据的日期,YYYY-MM-DD 格式 - 仅包含年、月、日"),
});
const rest = restClient(process.env.POLYGON_API_KEY);
const tickerTool = tool(async (query) => {
const parsed = TickerToolInputSchema.parse(query);
const result = await rest.stocks.aggregates(
parsed.ticker,
parsed.timespan_multiplier,
parsed.timespan,
parsed.from_date,
parsed.to_date
);
return JSON.stringify(result);
}, {
name: "ticker",
description: "为行情代码拉取数据",
schema: TickerToolInputSchema,
});
const searchTool = new TavilySearchResults({
maxResults: 5,
});
const AgentOutputFormatSchema = z.object({
numeric_answer: z.number().optional().describe("数值答案,如果用户要求的话"),
text_answer: z.string().optional().describe("文本答案,如果用户要求的话"),
reasoning: z.string().describe("答案背后的推理过程"),
})
const tools = [codeTool, searchTool, tickerTool];
const agent = createReactAgent({
llm: new ChatOpenAI({ model: "gpt-5.5" }),
tools: tools,
responseFormat: AgentOutputFormatSchema,
stateModifier: "你是一位金融专家。准确回答用户的查询。",
});
export default agent;
测试
测试代码
测试代码
# from app import agent, polygon_aggregates, search_tool # 从你定义智能体的地方导入
import pytest
from langchain.chat_models import init_chat_model
from langsmith import testing as t
from typing_extensions import Annotated, TypedDict
@pytest.mark.langsmith
@pytest.mark.parametrize(
# <-- 仍然可以使用所有正常的 pytest 标记
"query",
["你好!", "你好吗?"],
)
def test_no_tools_on_offtopic_query(query: str) -> None:
"""测试智能体在离题查询上不使用工具。"""
# 记录测试样例
t.log_inputs({"query": query})
expected = []
t.log_reference_outputs({"tool_calls": expected})
# 直接调用智能体的模型节点,而不是运行 ReACT 循环。
result = agent.nodes["agent"].invoke(
{"messages": [{"role": "user", "content": query}]}
)
actual = result["messages"][0].tool_calls
t.log_outputs({"tool_calls": actual})
# 检查是否没有进行工具调用。
assert actual == expected
@pytest.mark.langsmith
def test_searches_for_correct_ticker() -> None:
"""测试模型在简单查询中查找正确的股票代码。"""
# 记录测试样例
query = "苹果的股价是多少?"
t.log_inputs({"query": query})
expected = "AAPL"
t.log_reference_outputs({"ticker": expected})
# 直接调用智能体的模型节点,而不是运行完整的 ReACT 循环。
result = agent.nodes["agent"].invoke(
{"messages": [{"role": "user", "content": query}]}
)
tool_calls = result["messages"][0].tool_calls
if tool_calls[0]["name"] == polygon_aggregates.name:
actual = tool_calls[0]["args"]["ticker"]
else:
actual = None
t.log_outputs({"ticker": actual})
# 检查查询了正确的股票代码
assert actual == expected
@pytest.mark.langsmith
def test_executes_code_when_needed() -> None:
query = (
"过去一年中,Facebook 股价上涨了 66.76%,"
"苹果上涨了 25.24%,谷歌上涨了 37.11%,亚马逊上涨了 47.52%,"
"Netflix 上涨了 78.31%。FAANG 股票过去一年的平均回报率"
"是多少,以百分比表示?"
)
t.log_inputs({"query": query})
expected = 50.988
t.log_reference_outputs({"response": expected})
# 测试智能体在需要时执行代码
result = agent.invoke({"messages": [{"role": "user", "content": query}]})
t.log_outputs({"result": result["structured_response"].get("numeric_answer")})
# 获取 LLM 进行的所有工具调用
tool_calls = [
tc["name"]
for msg in result["messages"]
for tc in getattr(msg, "tool_calls", [])
]
# 这将记录智能体采取的步骤数,这对于确定智能体获得答案的效率很有用。
t.log_feedback(key="num_steps", score=len(result["messages"]) - 1)
# 断言使用了代码工具
assert "code_tool" in tool_calls
# 断言提供了数值答案:
assert result["structured_response"].get("numeric_answer") is not None
# 断言答案正确
assert abs(result["structured_response"]["numeric_answer"] - expected) <= 0.01
class Grade(TypedDict):
"""评估答案在源文档中的根基性。"""
score: Annotated[
bool,
...,
"如果答案完全基于源文档,则返回 True,否则返回 False。",
]
judge_llm = init_chat_model("gpt-5.5").with_structured_output(Grade)
@pytest.mark.langsmith
def test_grounded_in_source_info() -> None:
"""测试响应基于工具输出。"""
query = "根据分析师的说法,英伟达股票在 2024 年表现如何?"
t.log_inputs({"query": query})
result = agent.invoke({"messages": [{"role": "user", "content": query}]})
# 获取 LLM 进行的所有搜索调用
search_results = "\n\n".join(
msg.content
for msg in result["messages"]
if msg.type == "tool" and msg.name == search_tool.name
)
t.log_outputs(
{
"response": result["structured_response"].get("text_answer"),
"search_results": search_results,
}
)
# 将反馈 LLM 运行单独跟踪,与部署运行分开。
with t.trace_feedback():
# LLM 评判者的指令
instructions = (
"对以下答案进行评分。"
"答案应完全基于(即由源文档支持)源文档。"
"如果答案完全基于文档,则返回 True。"
"如果答案未基于文档,则返回 False。"
)
answer_and_docs = (
f"答案: {result['structured_response'].get('text_answer', '')}\n"
f"文档:\n{search_results}"
)
# 运行评判 LLM
grade = judge_llm.invoke(
[
{"role": "system", "content": instructions},
{"role": "user", "content": answer_and_docs},
]
)
t.log_feedback(key="groundedness", score=grade["score"])
assert grade["score"]
import { expect } from "vitest";
import * as ls from "langsmith/vitest";
import agent from "../agent";
import { AIMessage, ToolMessage } from "@langchain/core/messages";
import { ChatOpenAI } from "@langchain/openai";
const judgeLLM = new ChatOpenAI({ model: "gpt-5.5" });
const groundedEvaluator = async (params: {
answer: string;
referenceDocuments: string,
}) => {
const instructions = [
"如果答案基于文档,则返回 1",
"如果答案未基于文档,则返回 0",
].join("\n");
const grade = await judgeLLM.invoke([
{ role: "system", content: instructions },
{ role: "user", content: `答案: ${params.answer}\n文档: ${params.referenceDocuments}` },
]);
const score = parseInt(grade.content.toString());
return { key: "groundedness", score };
};
ls.describe("智能体测试", () => {
ls.test.each([
{ inputs: { query: "你好!" }, referenceOutputs: { numMessages: 2 } },
{ inputs: { query: "你好吗?" }, referenceOutputs: { numMessages: 2 } },
])(
"在离题查询上不应使用工具: %s",
async ({ inputs: { query }, referenceOutputs: { numMessages } }) => {
const result = await agent.invoke({
messages: [{ role: "user", content: query }],
});
ls.logOutputs(result);
expect(result.messages).toHaveLength(numMessages);
}
);
ls.test(
"应搜索正确的股票代码",
{
inputs: { query: "苹果的股价是多少?" },
referenceOutputs: { numMessages: 4 },
},
async ({ inputs: { query }, referenceOutputs: { numMessages } }) => {
const result = await agent.invoke({
messages: [{ role: "user", content: query }],
});
const toolCalls = (result.messages[1] as AIMessage).tool_calls || [];
const tickerQuery = toolCalls[0].args.ticker;
ls.logOutputs(result);
expect(tickerQuery).toBe("AAPL");
expect(result.messages).toHaveLength(numMessages);
}
);
ls.test(
"在需要时应执行代码",
{
inputs: { query: "2024年 FAANG 股票的平均回报率是多少?" },
referenceOutputs: { answer: 53 },
},
async ({ inputs: { query }, referenceOutputs: { answer } }) => {
const result = await agent.invoke({
messages: [{ role: "user", content: query }],
});
const toolCalls = result.messages
.filter(m => (m as AIMessage).tool_calls)
.flatMap(m => (m as AIMessage).tool_calls?.map(tc => tc.name));
ls.logFeedback({
key: "num_steps",
score: result.messages.length - 1,
});
ls.logOutputs(result);
expect(toolCalls).toContain("code_tool");
expect(Math.abs((result.structuredResponse.numeric_answer ?? 0) - answer)).toBeLessThanOrEqual(1);
}
);
ls.test(
"基于来源",
{
inputs: { query: "根据分析师的说法,英伟达股票在 2024 年表现如何?" },
referenceOutputs: {},
},
async ({ inputs: { query }, referenceOutputs: {} }) => {
const result = await agent.invoke({
messages: [{ role: "user", content: query }],
});
const referenceDocuments = result.messages
.filter((m): m is ToolMessage => m.name?.includes('tavily_search_results_json') ?? false)
.map(m => m.content)
.join('\n');
const wrappedEvaluator = ls.wrapEvaluator(groundedEvaluator);
await wrappedEvaluator({
answer: result.structuredResponse.text_answer ?? "",
referenceDocuments: referenceDocuments,
})
ls.logOutputs(result);
}
);
});
import { expect } from "@jest/globals";
import * as ls from "langsmith/jest";
import agent from "../agent";
import { AIMessage } from "@langchain/core/messages";
import { ChatOpenAI } from "@langchain/openai";
const judgeLLM = new ChatOpenAI({ model: "gpt-5.5" });
const groundedEvaluator = async (params: {
answer: string;
referenceDocuments: string,
}) => {
const instructions = [
"如果答案基于文档,则返回 1",
"如果答案未基于文档,则返回 0",
].join("\n");
const grade = await judgeLLM.invoke([
{ role: "system", content: instructions },
{ role: "user", content: `答案: ${params.answer}\n文档: ${params.referenceDocuments}` },
]);
const score = parseInt(grade.content.toString());
return { key: "groundedness", score };
};
ls.describe("智能体测试", () => {
ls.test.each([
{ inputs: { query: "你好!" }, referenceOutputs: { numMessages: 2 } },
{ inputs: { query: "你好吗?" }, referenceOutputs: { numMessages: 2 } },
])(
"在离题查询上不应使用工具: %s",
async ({ inputs: { query }, referenceOutputs: { numMessages } }) => {
const result = await agent.invoke({
messages: [{ role: "user", content: query }],
});
ls.logOutputs(result);
expect(result.messages).toHaveLength(numMessages);
}
);
ls.test(
"应搜索正确的股票代码",
{
inputs: { query: "苹果的股价是多少?" },
referenceOutputs: { numMessages: 4 },
},
async ({ inputs: { query }, referenceOutputs: { numMessages } }) => {
const result = await agent.invoke({
messages: [{ role: "user", content: query }],
});
const toolCalls = (result.messages[1] as AIMessage).tool_calls || [];
const tickerQuery = toolCalls[0].args.ticker;
ls.logOutputs(result);
expect(tickerQuery).toBe("AAPL");
expect(result.messages).toHaveLength(numMessages);
}
);
ls.test(
"在需要时应执行代码",
{
inputs: { query: "2024年 FAANG 股票的平均回报率是多少?" },
referenceOutputs: { answer: 53 },
},
async ({ inputs: { query }, referenceOutputs: { answer } }) => {
const result = await agent.invoke({
messages: [{ role: "user", content: query }],
});
const toolCalls = result.messages
.filter(m => (m as AIMessage).tool_calls)
.flatMap(m => (m as AIMessage).tool_calls?.map(tc => tc.name));
ls.logFeedback({
key: "num_steps",
score: result.messages.length - 1,
});
ls.logOutputs(result);
expect(toolCalls).toContain("code_tool");
expect(Math.abs((result.structuredResponse.numeric_answer ?? 0) - answer)).toBeLessThanOrEqual(1);
}
);
ls.test(
"基于来源",
{
inputs: { query: "根据分析师的说法,英伟达股票在 2024 年表现如何?" },
referenceOutputs: {},
},
async ({ inputs: { query }, referenceOutputs: {} }) => {
const result = await agent.invoke({
messages: [{ role: "user", content: query }],
});
const wrappedEvaluator = ls.wrapEvaluator(groundedEvaluator);
await wrappedEvaluator({
answer: result.structuredResponse.text_answer ?? "",
referenceDocuments: result.structuredResponse.reasoning,
})
ls.logOutputs(result);
}
);
});
Connect these docs to Claude, VSCode, and more via MCP for real-time answers.

