TL;DR: We're announcing improvements to our callbacks system, which powers logging, tracing, streaming output, and some awesome third-party integrations. This will better support concurrent runs with independent callbacks, tracing of deeply nested trees of LangChain components, and callback handlers scoped to a single request (which is super useful for deploying LangChain on a server).
Context
Originally we designed the callbacks mechanism in LangChain to be used in non-async Python applications. Now that we have support for both asyncio
Python usage as well LangChain in JavaScript/TypeScript, we needed some better abstractions native to this new world where many concurrent LangChain runs can be inflight in the same thread or in multiple threads. Additionally, it became clear that developers using LangChain in web environments often wanted to scope a callback to a single request (so they can pass it a specific handle to a websocket, for example).
Changes
We've made some changes to our callbacks mechanism to address these issues:
- You can now declare which callbacks you want either in constructor args (which apply to all runs), or passing them directly to the
run
/call
/apply
methods that start a run. Constructor callbacks will be used for all calls made on that object, and will be scoped to that object only, i.e. if you pass a handler to theLLMChain
constructor, it will not be used by the model attached to that chain. - Request callbacks will be used for that specific request only, and all sub-requests that it contains (eg. a call to an LLMChain triggers a call to a Model, which uses the same handler passed in the
call()
method). These are explicitly passed through. An example to make this more concrete: when a handler is passed through to anAgentExecutor
viarun
, it will be used for all callbacks related to the agent and all the objects involved in the agent’s execution, in this case, theTools
,LLMChain
, andLLM
. Previously, to use a callback scoped to particular agent run, that callback manager had to be attached to all nested objects – this was tedious, ugly, and made it hard to re-use objects. See the TypeScript example below:
// What had to be done before for run-scoped custom callbacks. Very tedious!
const executors = [];
for (let i = 0; i < 3; i += 1) {
const callbackManager = new CallbackManager();
callbackManager.addHandler(new ConsoleCallbackHandler());
callbackManager.addHandler(new LangChainTracer());
const model = new OpenAI({ temperature: 0, callbackManager });
const tools = [new SerpAPI(), new Calculator()];
for (const tool of tools) {
tool.callbackManager = callbackManager;
}
const executor = await initializeAgentExecutor(
tools,
model,
"zero-shot-react-description",
true,
callbackManager
);
executor.agent.llmChain.callbackManager = callbackManager;
executors.push(executor);
}
const results = await Promise.all(
executors.map((executor) => executor.call({ input }))
);
for (const result of results) {
console.log(`Got output ${result.output}`);
}
_call
,_generate
,_run
, and equivalent async methods on Chains / LLMs / Chat Models / Agents / Tools now receive a 2nd argument calledrunManager
which is bound to that run, and contains the logging methods that can be used by that object (i.e.handleLLMNewToken
). This is useful when constructing custom chains, for example, and you can find more info here.- The
verbose
argument now just serves as a shortcut to add aConsoleCallbackHandler
in JS andStdOutCallbackHandler
in python that prints events to stdout. It does not control other callbacks.
Tracing and other callbacks now just work with concurrency. We've also added a context manager to make tracing specific runs even easier.
Breaking Changes and Deprecations:
- Any code that relied on global callbacks or the global tracer (i.e.
SharedCallbackManager
,SharedTracer
) outside of LangChain will break in versions >0.0.153 of the python package. - Attaching a
CallbackManager
to an object is now deprecated, use thecallbacks
argument to pass in a list of handlers. - The
verbose
flag now only controlsstdout
andconsole
callbacks, not other callbacks.
Inspiration
When we were implementing these improvements to Callbacks we looked at a few existing solutions that ended up influencing the final API, worth calling out:
- The Python
logging
module (and others), which offers agetChild
method that returns a new logger bound to a certain context. This inspired the newrunManager.getChild()
which you can use when implementing a custom Chain to ensure child runs are tracked correctly. - Web server frameworks like
express
where all the context specific to each HTTP request is passed around explicitly as function arguments, rather than being available as some sort of global variable.
We also considered the alternative of using some form of async context variables, an implementation of which exists in Python and in Node.js (but not in other JS environments). In the end we decided for the explicit function arguments approach because it is easier to debug, and more compatible cross-platform (function args work just about anywhere).
Please let us know if you run into any issues, as this was a large change!