Skip to content

Commit 7c0a03c

Browse files
committed
chore: langgraphjs + local model
1 parent 02d9435 commit 7c0a03c

12 files changed

+882
-64
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ npm i
2424
# Option 1: use local llm, configure the `baseURL` in code then run
2525
npx tsx ./langchain/chain-groq1-chat-local-mini.ts
2626

27-
# Option 2: use groq api, configure the `groq_api_key` first
27+
# Option 2: use groq api, configure the `GROQ_API_KEY` first
2828
cp .env.example .env
2929
npx tsx ./server/chain-groq1-starter.ts
3030
```
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ChatOpenAI } from '@langchain/openai';
2+
3+
const llm = new ChatOpenAI({
4+
model: 'qwen/qwen3-4b-2507',
5+
configuration: {
6+
baseURL: 'http://localhost:1234/v1',
7+
apiKey: 'not-needed',
8+
},
9+
temperature: 0,
10+
});
11+
12+
const res = await llm.invoke([{ role: 'user', content: "hello, I'm York." }]);
13+
console.log(res);
14+
15+
// 👀 The model on its own does not have any concept of state. response: I don't know who you are
16+
// const res2 = await llm.invoke([{ role: "user", content: "What's my name?" }]);
17+
// console.log(res2);
18+
19+
// ✅ To get around this, we need to pass the entire conversation history into the model.
20+
const res3 = await llm.invoke([
21+
{ role: 'user', content: "Hello, I'm York" },
22+
{ role: 'assistant', content: 'Hello York! How can I assist you today?' },
23+
{ role: 'user', content: "What's my name?" },
24+
]);
25+
console.log(res3);
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { ChatOpenAI } from '@langchain/openai';
2+
3+
import {
4+
START,
5+
END,
6+
MessagesAnnotation,
7+
StateGraph,
8+
MemorySaver,
9+
} from '@langchain/langgraph';
10+
11+
import { ulid } from 'ulid';
12+
13+
const llm = new ChatOpenAI({
14+
model: 'qwen/qwen3-4b-2507',
15+
configuration: {
16+
baseURL: 'http://localhost:1234/v1',
17+
apiKey: 'not-needed',
18+
},
19+
temperature: 0,
20+
});
21+
22+
// Define the function that calls the model
23+
const callModel = async (state: typeof MessagesAnnotation.State) => {
24+
const response = await llm.invoke(state.messages);
25+
return { messages: response };
26+
};
27+
28+
// Define a new graph
29+
const workflow = new StateGraph(MessagesAnnotation)
30+
// Define the node and edge
31+
.addNode('model', callModel)
32+
.addEdge(START, 'model')
33+
.addEdge('model', END);
34+
35+
// Add memory
36+
const memory = new MemorySaver();
37+
const app = workflow.compile({ checkpointer: memory });
38+
39+
const config = { configurable: { thread_id: ulid() } };
40+
41+
const input = [
42+
{
43+
role: 'user',
44+
content: "Hi! I'm York.",
45+
},
46+
];
47+
// The `output` contains all messages in the state.
48+
const output = await app.invoke({ messages: input }, config);
49+
50+
console.log('\n👾');
51+
console.log(output.messages[output.messages.length - 1]);
52+
53+
const input2 = [
54+
{
55+
role: 'user',
56+
content: "What's my name?",
57+
},
58+
];
59+
const output2 = await app.invoke({ messages: input2 }, config);
60+
console.log('\n👾');
61+
console.log(output2.messages[output2.messages.length - 1]);
62+
63+
const config2 = { configurable: { thread_id: ulid() } };
64+
const input3 = [
65+
{
66+
role: 'user',
67+
content: "What's my name?",
68+
},
69+
];
70+
const output3 = await app.invoke({ messages: input3 }, config2);
71+
console.log('\n👾');
72+
console.log(output3.messages[output3.messages.length - 1]);

langchain/chain-groq1-chat-local-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { HumanMessage } from '@langchain/core/messages';
44

55
import { initChatModel } from 'langchain/chat_models/universal';
66

7-
// ❌ not working
7+
// ❌ not working with local model
88
const model = await initChatModel('qwen/qwen3-4b-2507', {
99
modelProvider: 'openai',
1010
baseUrl: 'http://localhost:1234/v1',
Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,57 @@
1-
import '@dotenvx/dotenvx/config';
1+
// import '@dotenvx/dotenvx/config';
22

33
import { z } from 'zod';
44

5-
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
65
import { ChatGroq } from '@langchain/groq';
6+
import { ChatOpenAI } from '@langchain/openai';
77

8-
const computerTopic = z.object({
9-
// syntax: z.string().describe("The syntax"),
10-
briefDescription: z.string().describe('Brief description'),
11-
usageDetails: z.string().optional().describe('Usage details or examples'),
8+
// const model = new ChatGroq({
9+
// model: 'meta-llama/llama-4-scout-17b-16e-instruct',
10+
// temperature: 0,
11+
// });
12+
const model = new ChatOpenAI({
13+
// model: 'qwen/qwen3-4b-2507',
14+
model: 'google/gemma-3-12b',
15+
configuration: {
16+
baseURL: 'http://localhost:1234/v1',
17+
apiKey: 'not-needed',
18+
},
19+
temperature: 0.5,
1220
});
1321

14-
const model = new ChatGroq({
15-
model: 'meta-llama/llama-4-scout-17b-16e-instruct',
16-
temperature: 0,
22+
const phoneDevice = z.object({
23+
name: z.string().describe('Device name'),
24+
description: z.string().describe('Brief description'),
25+
details: z.string().describe('Device details or use cases'),
1726
});
1827

19-
// 💡 we can pass a name for our schema in order to give the model additional context as to what our schema represents
20-
// const structuredLlm = model.withStructuredOutput(computerTopic, { name: 'computerTopic' });
21-
// const res = await structuredLlm.invoke("introduce sort algorithms");
22-
23-
// 💡 We can also pass in an OpenAI-style JSON schema dict if you prefer not to use Zod
24-
const structuredLlm = model.withStructuredOutput({
25-
name: 'computerTopic',
26-
descripttion: 'knowledge about computer',
27-
parameters: {
28-
title: 'computerTopic',
29-
type: 'object',
30-
properties: {
31-
briefDescription: { type: 'string', description: 'Brief description' },
32-
details: { type: 'string', description: 'Usage details or examples' },
33-
},
34-
required: ['briefDescription', 'details'],
35-
},
36-
});
37-
const res = await structuredLlm.invoke('introduce sort algorithms', {
38-
// @ts-expect-error llm-topic
39-
name: 'computerTopic',
28+
// 💡 Option 1: we can pass a name for our schema in order to give the model additional context as to what our schema represents
29+
const structuredLlm = model.withStructuredOutput(phoneDevice, {
30+
name: 'phoneDevice',
4031
});
32+
const res = await structuredLlm.invoke(
33+
'give a brief intro to a popular mobile phone',
34+
);
35+
36+
// 💡 Option 2: We can also pass in an OpenAI-style JSON schema dict if you prefer not to use Zod
37+
// 👀 大多数本地模型不支持类似下面json schema的方式,但线上模型支持
38+
// const structuredLlm = model.withStructuredOutput({
39+
// name: 'phoneDevice',
40+
// descripttion: 'cellphone device intro',
41+
// parameters: {
42+
// name: 'phoneDevice',
43+
// type: 'object',
44+
// properties: {
45+
// name: { type: 'string', description: 'Device name' },
46+
// description: { type: 'string', description: 'Brief description to device' },
47+
// details: { type: 'string', description: 'Device details or use cases' },
48+
// },
49+
// required: ['name', 'description'],
50+
// },
51+
// });
52+
// const res = await structuredLlm.invoke('give a brief intro to a popular mobile phone', {
53+
// // @ts-expect-error llm-topic
54+
// name: 'phoneDevice',
55+
// });
4156

4257
console.log(res);

langchain/chain-groq3-tool-call.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import '@dotenvx/dotenvx/config';
1+
// import '@dotenvx/dotenvx/config';
22

33
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
44
import { tool } from '@langchain/core/tools';
55
import { ChatGroq } from '@langchain/groq';
6+
import { ChatOpenAI } from '@langchain/openai';
67
import { z } from 'zod';
78

89
/**
@@ -39,11 +40,20 @@ const calculatorTool = tool(
3940
},
4041
);
4142

42-
const llm = new ChatGroq({
43-
model: 'meta-llama/llama-4-scout-17b-16e-instruct',
44-
temperature: 0,
43+
// const llm = new ChatGroq({
44+
// model: 'meta-llama/llama-4-scout-17b-16e-instruct',
45+
// temperature: 0,
46+
// });
47+
const llm = new ChatOpenAI({
48+
model: 'qwen/qwen3-4b-2507',
49+
configuration: {
50+
baseURL: 'http://localhost:1234/v1',
51+
apiKey: 'not-needed',
52+
},
53+
temperature: 0.5,
4554
});
4655

56+
// conversion from LangChain tool to our model provider’s specific format
4757
const llmWithTools = llm.bindTools([calculatorTool]);
4858

4959
// const res = await llmWithTools.invoke("What is 11 * 22");

langgraph/graph-groq1-starter.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import '@dotenvx/dotenvx/config';
1+
// import '@dotenvx/dotenvx/config';
22

33
import { tool } from '@langchain/core/tools';
44
import { ChatGroq } from '@langchain/groq';
55
import { createReactAgent } from '@langchain/langgraph/prebuilt';
6+
import { ChatOpenAI } from '@langchain/openai';
67

78
import { z } from 'zod';
89

@@ -25,9 +26,18 @@ const search = tool(
2526
},
2627
);
2728

28-
const model = new ChatGroq({
29-
model: 'meta-llama/llama-4-scout-17b-16e-instruct',
30-
temperature: 0,
29+
// const model = new ChatGroq({
30+
// model: 'meta-llama/llama-4-scout-17b-16e-instruct',
31+
// temperature: 0,
32+
// });
33+
34+
const model = new ChatOpenAI({
35+
model: 'qwen/qwen3-4b-2507',
36+
configuration: {
37+
baseURL: 'http://localhost:1234/v1',
38+
apiKey: 'not-needed',
39+
},
40+
temperature: 0.5,
3141
});
3242

3343
const agent = createReactAgent({

langgraph/graph-guide1-create-react-agent.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,37 @@ import { HumanMessage } from '@langchain/core/messages';
44
import { ChatGroq } from '@langchain/groq';
55
import { MemorySaver } from '@langchain/langgraph';
66
import { createReactAgent } from '@langchain/langgraph/prebuilt';
7+
import { ChatOpenAI } from '@langchain/openai';
78
import { TavilySearch } from '@langchain/tavily';
89

9-
// import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
10-
// const agentTools = [new TavilySearchResults({ maxResults: 2 })];
11-
// const agentModel = new ChatOpenAI({ temperature: 0 });
12-
1310
const agentTools = [new TavilySearch({ maxResults: 2 })];
14-
const agentModel = new ChatGroq({
15-
model: 'meta-llama/llama-4-scout-17b-16e-instruct',
11+
// const model = new ChatGroq({
12+
// model: 'meta-llama/llama-4-scout-17b-16e-instruct',
13+
// temperature: 0,
14+
// });
15+
// 👷👀 local qwen3-4b failed tool-call, local gemma3-12b succeeded
16+
const model = new ChatOpenAI({
17+
// model: 'qwen/qwen3-4b-2507',
18+
model: 'google/gemma-3-12b',
19+
configuration: {
20+
baseURL: 'http://localhost:1234/v1',
21+
apiKey: 'not-needed',
22+
},
1623
temperature: 0,
1724
});
1825

1926
// Initialize memory to persist state between graph runs
2027
const agentCheckpointer = new MemorySaver();
2128
const agent = createReactAgent({
22-
llm: agentModel,
29+
llm: model,
2330
tools: agentTools,
2431
checkpointSaver: agentCheckpointer,
2532
});
2633

2734
const agentFinalState = await agent.invoke(
28-
{ messages: [new HumanMessage('what is the current weather in guangzhou')] },
35+
{
36+
messages: [new HumanMessage('what is the current weather in guangzhou ?')],
37+
},
2938
{ configurable: { thread_id: '42' } },
3039
);
3140

@@ -34,7 +43,7 @@ console.log(
3443
);
3544

3645
const agentNextState = await agent.invoke(
37-
{ messages: [new HumanMessage('what about Beijing')] },
46+
{ messages: [new HumanMessage('what about Beijing ?')] },
3847
{ configurable: { thread_id: '42' } },
3948
);
4049

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import '@dotenvx/dotenvx/config';
2+
3+
import { AIMessage, HumanMessage } from '@langchain/core/messages';
4+
import {
5+
MemorySaver,
6+
MessagesAnnotation,
7+
StateGraph,
8+
} from '@langchain/langgraph';
9+
import { createReactAgent, ToolNode } from '@langchain/langgraph/prebuilt';
10+
import { ChatOpenAI } from '@langchain/openai';
11+
import { TavilySearch } from '@langchain/tavily';
12+
13+
const tools = [new TavilySearch({ maxResults: 2 })];
14+
// 👷👀 local qwen3-4b failed tool-call, local gemma3-12b succeeded
15+
const llm = new ChatOpenAI({
16+
// model: 'qwen/qwen3-4b-2507',
17+
model: 'google/gemma-3-12b',
18+
configuration: {
19+
baseURL: 'http://localhost:1234/v1',
20+
apiKey: 'not-needed',
21+
},
22+
temperature: 0,
23+
});
24+
const model = llm.bindTools(tools);
25+
26+
// Define the function that determines whether to call tools or not
27+
function shouldContinue({ messages }: typeof MessagesAnnotation.State) {
28+
const lastMessage = messages[messages.length - 1] as AIMessage;
29+
30+
// If the LLM makes a tool call, then we route to the "tools" node
31+
if (lastMessage.tool_calls?.length) {
32+
return 'tools';
33+
}
34+
// Otherwise, we stop (reply to the user) using the special "__end__" node
35+
return '__end__';
36+
}
37+
38+
// Define the function that calls the model
39+
async function callModel(state: typeof MessagesAnnotation.State) {
40+
const response = await model.invoke(state.messages);
41+
42+
// We return a list, because this will get added to the existing list
43+
return { messages: [response] };
44+
}
45+
46+
const toolNode = new ToolNode(tools);
47+
48+
const workflow = new StateGraph(MessagesAnnotation)
49+
.addNode('agent', callModel)
50+
.addNode('tools', toolNode)
51+
.addEdge('__start__', 'agent') // __start__ is a special name for the entrypoint
52+
.addEdge('tools', 'agent')
53+
.addConditionalEdges('agent', shouldContinue);
54+
55+
const agentCheckpointer = new MemorySaver();
56+
const graph = workflow.compile();
57+
// 👀 不使用memory时, ai也能通过tool call查询beijing天气
58+
// const graph = workflow.compile({checkpointer: agentCheckpointer});
59+
60+
const finalState = await graph.invoke({
61+
messages: [new HumanMessage('what is the weather in guangzhou ?')],
62+
});
63+
console.log(finalState.messages[finalState.messages.length - 1].content);
64+
65+
const nextState = await graph.invoke({
66+
// Including the messages from the previous run gives the LLM context.
67+
// This way it knows we're asking about the weather in NY
68+
messages: [...finalState.messages, new HumanMessage('what about Beijing ?')],
69+
});
70+
console.log(nextState.messages[nextState.messages.length - 1].content);

0 commit comments

Comments
 (0)