Migrate to LangChain from raw OpenAI SDK, LlamaIndex, or custom LLM code. Covers codebase assessment, side-by-side validation, RAG migration, agent migration, and feature-flagged gradual rollout. Trigger: "migrate to langchain", "langchain refactor", "legacy LLM migration", "replace openai SDK with langchain", "llamaindex to langchain".
84
82%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advisory
Suggest reviewing before use
!npm list 2>/dev/null | grep -E "openai|langchain|llamaindex" | head -10
Migrate from raw SDK calls (OpenAI, Anthropic) or other frameworks (LlamaIndex, custom agents) to LangChain. Covers codebase scanning, pattern-by-pattern migration, side-by-side validation, and feature-flagged rollout.
// Scan for migration targets
import * as fs from "fs";
import * as path from "path";
interface MigrationItem {
file: string;
line: number;
pattern: string;
complexity: "low" | "medium" | "high";
}
function scanForMigration(dir: string): MigrationItem[] {
const items: MigrationItem[] = [];
const patterns = [
{ regex: /openai\.chat\.completions\.create/g, name: "OpenAI direct call", complexity: "low" as const },
{ regex: /new OpenAI\(/g, name: "OpenAI SDK init", complexity: "low" as const },
{ regex: /anthropic\.messages\.create/g, name: "Anthropic direct call", complexity: "low" as const },
{ regex: /from llama_index/g, name: "LlamaIndex import", complexity: "medium" as const },
{ regex: /VectorStoreIndex/g, name: "LlamaIndex vector store", complexity: "high" as const },
{ regex: /function_call|tool_choice/g, name: "Manual tool calling", complexity: "high" as const },
];
// Recursive file scan
function scan(dir: string) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
scan(path.join(dir, entry.name));
} else if (entry.name.match(/\.(ts|js|py)$/)) {
const content = fs.readFileSync(path.join(dir, entry.name), "utf-8");
const lines = content.split("\n");
for (const p of patterns) {
lines.forEach((line, i) => {
if (p.regex.test(line)) {
items.push({ file: path.join(dir, entry.name), line: i + 1, pattern: p.name, complexity: p.complexity });
}
p.regex.lastIndex = 0;
});
}
}
}
}
scan(dir);
return items;
}import OpenAI from "openai";
const client = new OpenAI();
async function summarize(text: string) {
const response = await client.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{ role: "system", content: "Summarize the text." },
{ role: "user", content: text },
],
temperature: 0,
});
return response.choices[0].message.content;
}import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
const summarizeChain = ChatPromptTemplate.fromMessages([
["system", "Summarize the text."],
["human", "{text}"],
])
.pipe(new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 }))
.pipe(new StringOutputParser());
// Benefits gained:
// - .invoke(), .batch(), .stream() for free
// - Automatic retry with backoff
// - LangSmith tracing
// - .withFallbacks() for provider resilience
// - .withStructuredOutput() for typed resultsconst response = await client.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: input }],
tools: [{
type: "function",
function: {
name: "search",
parameters: { type: "object", properties: { query: { type: "string" } } },
},
}],
});
// Manual tool call loop
if (response.choices[0].message.tool_calls) {
for (const tc of response.choices[0].message.tool_calls) {
const result = await callTool(tc.function.name, JSON.parse(tc.function.arguments));
// Manually append tool result, re-call API...
}
}import { tool } from "@langchain/core/tools";
import { createToolCallingAgent, AgentExecutor } from "langchain/agents";
import { z } from "zod";
const searchTool = tool(
async ({ query }) => { /* search logic */ return "results"; },
{ name: "search", description: "Search the web", schema: z.object({ query: z.string() }) }
);
const agent = createToolCallingAgent({ llm, tools: [searchTool], prompt });
const executor = new AgentExecutor({ agent, tools: [searchTool] });
// One call handles the entire tool-calling loop
const result = await executor.invoke({ input: "Search for LangChain news", chat_history: [] });// Manual: embed -> search -> format -> call LLM
const queryEmbed = await openai.embeddings.create({ model: "text-embedding-3-small", input: query });
const results = await pineconeIndex.query({ vector: queryEmbed.data[0].embedding, topK: 5 });
const context = results.matches.map((m) => m.metadata.text).join("\n");
const answer = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: `Context: ${context}\n\nQuestion: ${query}` }],
});import { PineconeStore } from "@langchain/pinecone";
import { OpenAIEmbeddings, ChatOpenAI } from "@langchain/openai";
import { RunnableSequence, RunnablePassthrough } from "@langchain/core/runnables";
const vectorStore = await PineconeStore.fromExistingIndex(
new OpenAIEmbeddings({ model: "text-embedding-3-small" }),
{ pineconeIndex: index }
);
const ragChain = RunnableSequence.from([
{
context: vectorStore.asRetriever({ k: 5 }).pipe(
(docs) => docs.map((d) => d.pageContent).join("\n")
),
question: new RunnablePassthrough(),
},
ragPrompt,
new ChatOpenAI({ model: "gpt-4o-mini" }),
new StringOutputParser(),
]);
// Now you get: streaming, batching, tracing, fallbacks for freeasync function validateMigration(
legacyFn: (input: string) => Promise<string>,
newChain: any,
testInputs: string[],
) {
const results = [];
for (const input of testInputs) {
const [legacy, migrated] = await Promise.all([
legacyFn(input),
newChain.invoke({ input }),
]);
results.push({
input: input.slice(0, 50),
legacyLength: legacy.length,
migratedLength: migrated.length,
match: legacy.toLowerCase().includes(migrated.toLowerCase().slice(0, 20)),
});
}
console.table(results);
return results;
}function shouldUseLangChain(userId: string, rolloutPercent: number): boolean {
// Consistent hashing: same user always gets same experience
const hash = userId.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0);
return (hash % 100) < rolloutPercent;
}
async function processRequest(userId: string, input: string) {
if (shouldUseLangChain(userId, 25)) { // 25% rollout
return newChain.invoke({ input });
}
return legacySummarize(input);
}
// Gradual rollout: 10% -> 25% -> 50% -> 100%| Issue | Fix |
|---|---|
| Different response format | Add StringOutputParser or .withStructuredOutput() |
| Missing streaming | Use .stream() instead of .invoke() |
| Memory format mismatch | Use RunnableWithMessageHistory |
| Tool schema differences | Define with Zod, use tool() function |
Use langchain-upgrade-migration for LangChain version-to-version upgrades.
70e9fa4
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.